mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
445 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1b56fed16 | ||
|
|
23e75c56ed | ||
|
|
eafc9cc1fa | ||
|
|
3e3dbb22df | ||
|
|
127b8ba0bd | ||
|
|
8a8e588a01 | ||
|
|
3c1b010821 | ||
|
|
739c938576 | ||
|
|
69fed0c347 | ||
|
|
d040258296 | ||
|
|
abee18839b | ||
|
|
9c0eb4e27e | ||
|
|
10598ef5e5 | ||
|
|
fa09e2d21d | ||
|
|
11ddfe3445 | ||
|
|
9abbc8876b | ||
|
|
c5cf99e13d | ||
|
|
82ac1c8f5e | ||
|
|
837f8146e4 | ||
|
|
9698c3f2ee | ||
|
|
85290b8f9d | ||
|
|
1ccf48d7a5 | ||
|
|
2cbec9f2b6 | ||
|
|
692be415fa | ||
|
|
83d5218f92 | ||
|
|
324b016324 | ||
|
|
005c9673f7 | ||
|
|
b8762dc6e0 | ||
|
|
93abff0768 | ||
|
|
ac1cfb43d8 | ||
|
|
30ec712782 | ||
|
|
bbd93fbc5b | ||
|
|
c46cbaff4b | ||
|
|
1e979b256a | ||
|
|
50429b419f | ||
|
|
2c540be5a3 | ||
|
|
155d3d222f | ||
|
|
3fc3bf1302 | ||
|
|
f0fafbbc6e | ||
|
|
5f7d980d89 | ||
|
|
c3799ed7fe | ||
|
|
f7533af5d8 | ||
|
|
2c9504fb93 | ||
|
|
c22b3f5335 | ||
|
|
2aea7fd46d | ||
|
|
bb9dda0cf1 | ||
|
|
ced6f05e59 | ||
|
|
cc14b5d4d4 | ||
|
|
2922f7110d | ||
|
|
762a877c5e | ||
|
|
aace7a8260 | ||
|
|
ac91abcb45 | ||
|
|
5d6ccd84c9 | ||
|
|
bd2f980f8e | ||
|
|
69af330ba1 | ||
|
|
b7bc3bfa8f | ||
|
|
678663ad66 | ||
|
|
ec3aa14112 | ||
|
|
3cdd98dc1a | ||
|
|
fc3270efd2 | ||
|
|
0dd871d3b5 | ||
|
|
41108ff407 | ||
|
|
99de8cf220 | ||
|
|
a60e887eb3 | ||
|
|
5133cb5c8f | ||
|
|
1607d84c21 | ||
|
|
cc0a990af8 | ||
|
|
fec5bdcfc8 | ||
|
|
648514d150 | ||
|
|
37b9745b6b | ||
|
|
f858b1144c | ||
|
|
c7cd7b83fa | ||
|
|
3cef086b69 | ||
|
|
ca51f591de | ||
|
|
da63404a84 | ||
|
|
de2160f992 | ||
|
|
8b7bb36e66 | ||
|
|
6ade91dafa | ||
|
|
2ef66f3011 | ||
|
|
5b2fd0bfbb | ||
|
|
ab392e2cf3 | ||
|
|
8eb922e93f | ||
|
|
20900dce07 | ||
|
|
c2705ff5d2 | ||
|
|
8af70fa7b5 | ||
|
|
e7eb8099ac | ||
|
|
cedf2eafbb | ||
|
|
e7553c68a0 | ||
|
|
cea7f1c2d1 | ||
|
|
45937f9c9c | ||
|
|
ee2a1ea924 | ||
|
|
1851c205e9 | ||
|
|
c4a3947cb3 | ||
|
|
d3e76bcf21 | ||
|
|
cf4e64d6c5 | ||
|
|
4cac571f1b | ||
|
|
445ed92ff7 | ||
|
|
6686ce15c1 | ||
|
|
59f134e0cd | ||
|
|
aab3f8c56f | ||
|
|
17c26e2a96 | ||
|
|
aec9124ef5 | ||
|
|
2df1e9bc2e | ||
|
|
6ceecbfd0f | ||
|
|
460726cadb | ||
|
|
32baa7b9f1 | ||
|
|
f44e648ccc | ||
|
|
2ad385acb6 | ||
|
|
ebaec2eaf0 | ||
|
|
dac8c2c15d | ||
|
|
60965491b0 | ||
|
|
19a3faa85a | ||
|
|
3779346eed | ||
|
|
185e263ddd | ||
|
|
8c0d984955 | ||
|
|
8d1012eda0 | ||
|
|
03f147e3d0 | ||
|
|
13ef5d640c | ||
|
|
2e8eed5afe | ||
|
|
54653a5bea | ||
|
|
e29082eba3 | ||
|
|
b635e9bb0d | ||
|
|
179bfdc3d2 | ||
|
|
631cb104cd | ||
|
|
1aeaf6855e | ||
|
|
6bac1ace30 | ||
|
|
488bab7c4d | ||
|
|
69fbfd7723 | ||
|
|
7697713c44 | ||
|
|
ee9b072f45 | ||
|
|
741defd31e | ||
|
|
e259b37f74 | ||
|
|
9a87de797c | ||
|
|
5d44ba658e | ||
|
|
00518b8231 | ||
|
|
6a3a99d2ac | ||
|
|
819434968c | ||
|
|
9f55ca2fdb | ||
|
|
7f69563edb | ||
|
|
b5cf0f987e | ||
|
|
28ad0b39c3 | ||
|
|
b4e9040d5c | ||
|
|
18a6ff0aa5 | ||
|
|
66e8c25265 | ||
|
|
aba1f2d35b | ||
|
|
5f29bcea8f | ||
|
|
fac6fd7926 | ||
|
|
5f4ab201af | ||
|
|
9591d36f9d | ||
|
|
a38f34995d | ||
|
|
f72564e48d | ||
|
|
39ae743c64 | ||
|
|
9cdb355878 | ||
|
|
6bdabbc96b | ||
|
|
d9e7d5ff6f | ||
|
|
9f41da7868 | ||
|
|
d4f7258ed1 | ||
|
|
aa34d78052 | ||
|
|
72712e8e0e | ||
|
|
bf25054ef2 | ||
|
|
32efafb404 | ||
|
|
3748ba1afa | ||
|
|
5baf91dc77 | ||
|
|
a152a48402 | ||
|
|
5fb4c4c20c | ||
|
|
80cf4f05f8 | ||
|
|
ce8e532f61 | ||
|
|
7e04fcbb6c | ||
|
|
71c37d0b8b | ||
|
|
518375e29a | ||
|
|
25c845c1c7 | ||
|
|
6a468b339f | ||
|
|
05a52dd482 | ||
|
|
5f1413ea1f | ||
|
|
84e45482a4 | ||
|
|
32bfdca562 | ||
|
|
547971545e | ||
|
|
c12b16faf9 | ||
|
|
858d6c8723 | ||
|
|
0f021fae9f | ||
|
|
5cb42264c5 | ||
|
|
c17af4098f | ||
|
|
e6c094b433 | ||
|
|
b15a86618b | ||
|
|
2e66cee551 | ||
|
|
c6e3d0125a | ||
|
|
f60f7d32dc | ||
|
|
dd7175d0c6 | ||
|
|
ba0e87f32a | ||
|
|
cebba5a09d | ||
|
|
7b0c31c641 | ||
|
|
487be74e83 | ||
|
|
3c7c88e0f5 | ||
|
|
e876e89b41 | ||
|
|
065fd9632c | ||
|
|
f7d8641e31 | ||
|
|
ed6456599b | ||
|
|
649538846a | ||
|
|
7468db5269 | ||
|
|
8291e94de4 | ||
|
|
94d30fc7ec | ||
|
|
36c6f371c5 | ||
|
|
94b3f66e14 | ||
|
|
c9527ddacd | ||
|
|
805c728e92 | ||
|
|
7812a86adb | ||
|
|
d22defcd83 | ||
|
|
d06829ff7b | ||
|
|
9110a1aca6 | ||
|
|
d7c90601d0 | ||
|
|
7b5fc600f5 | ||
|
|
0e699a1918 | ||
|
|
49f9964bc7 | ||
|
|
ee3ec4b14d | ||
|
|
cdcc9f0aff | ||
|
|
d56a01998f | ||
|
|
4e7f7f7fa0 | ||
|
|
201d8f8aee | ||
|
|
3847cb4d2d | ||
|
|
89a338ac33 | ||
|
|
0084d113d3 | ||
|
|
6d49cc3577 | ||
|
|
910c45a6be | ||
|
|
1d16ad764f | ||
|
|
142021c849 | ||
|
|
c87b9768b1 | ||
|
|
fbc3c1a9a4 | ||
|
|
05d1656a9e | ||
|
|
98804db478 | ||
|
|
c90f18c45a | ||
|
|
553783bfce | ||
|
|
fbf67f28f5 | ||
|
|
b8005466cd | ||
|
|
82177a386e | ||
|
|
176b276835 | ||
|
|
a4732194ef | ||
|
|
4a815daf4d | ||
|
|
8aa7804e8d | ||
|
|
3b7086627d | ||
|
|
20fb4036d1 | ||
|
|
3679ad3d70 | ||
|
|
63f23c571f | ||
|
|
7e96303c7d | ||
|
|
4f5879179e | ||
|
|
5a81f54a1b | ||
|
|
d8ccaf3578 | ||
|
|
7d0848f22b | ||
|
|
b02cffab56 | ||
|
|
59b6deb7be | ||
|
|
1b734051d5 | ||
|
|
037fe41961 | ||
|
|
7bf1263c04 | ||
|
|
0e4afd4681 | ||
|
|
21930021ed | ||
|
|
748b5c54c9 | ||
|
|
82d305008b | ||
|
|
1f24955533 | ||
|
|
ccfa28b895 | ||
|
|
2e3562d87e | ||
|
|
f1be109c15 | ||
|
|
b09bd39ad4 | ||
|
|
d812c3d61b | ||
|
|
0d7772a4c6 | ||
|
|
a5abf4d186 | ||
|
|
c938a13482 | ||
|
|
0bfc395986 | ||
|
|
60c536f444 | ||
|
|
8129e6e0c1 | ||
|
|
223a8e9a5e | ||
|
|
0e21c75007 | ||
|
|
6ca031e8fd | ||
|
|
f02e86fb50 | ||
|
|
b17f8244da | ||
|
|
4c02ce138b | ||
|
|
5eba208b00 | ||
|
|
2a4fbbfb35 | ||
|
|
5dd8bdcae6 | ||
|
|
c3e82eea5d | ||
|
|
2cfb96be33 | ||
|
|
715eedfab1 | ||
|
|
7f3a1c9a7d | ||
|
|
8413d2b31a | ||
|
|
04a03da382 | ||
|
|
e3274657ea | ||
|
|
f3b25cb792 | ||
|
|
d181f886fe | ||
|
|
616d073395 | ||
|
|
d36fc19585 | ||
|
|
95d9e07e2f | ||
|
|
25077a7b9a | ||
|
|
6971cd1a6b | ||
|
|
1e43a65743 | ||
|
|
2f2cbf343e | ||
|
|
3426b3cdeb | ||
|
|
e1d4c172f4 | ||
|
|
4c4a67afaf | ||
|
|
d33f210940 | ||
|
|
ead59bd410 | ||
|
|
f9445de71f | ||
|
|
c26995779a | ||
|
|
c6277453a3 | ||
|
|
91ebf2ba6f | ||
|
|
0f87bdd5a3 | ||
|
|
9e97042dd1 | ||
|
|
b7c4a99e71 | ||
|
|
48e315453e | ||
|
|
a8a6d14ca3 | ||
|
|
e895dd3430 | ||
|
|
f59859137a | ||
|
|
dee92e9e40 | ||
|
|
6701f4f95e | ||
|
|
e20f769854 | ||
|
|
4f762a9432 | ||
|
|
3c49eb1635 | ||
|
|
bdc6a282e2 | ||
|
|
8392ab2cc4 | ||
|
|
93c7c09f8c | ||
|
|
120116414f | ||
|
|
11794e5819 | ||
|
|
edce3d7bec | ||
|
|
e133e32e7c | ||
|
|
471859e448 | ||
|
|
d8de66eb14 | ||
|
|
cfdc0237d7 | ||
|
|
05fad24eda | ||
|
|
d4818c5567 | ||
|
|
8e8e6a7b93 | ||
|
|
6547f0ffad | ||
|
|
6f172fffa8 | ||
|
|
ed16e06676 | ||
|
|
1874f06f42 | ||
|
|
e9db24429a | ||
|
|
a59f4d45ca | ||
|
|
c7b3e0926c | ||
|
|
f0f5258bc9 | ||
|
|
12c07cf793 | ||
|
|
d2b8c85015 | ||
|
|
b9652291bd | ||
|
|
b0d1f93bfc | ||
|
|
a6d6c247a8 | ||
|
|
553416c927 | ||
|
|
b83696bc60 | ||
|
|
23ce320d75 | ||
|
|
dd170aafee | ||
|
|
27d5733dbc | ||
|
|
9ba769c53e | ||
|
|
8ff57e3004 | ||
|
|
5c75c6c7d3 | ||
|
|
56efb20ffa | ||
|
|
699578bb59 | ||
|
|
8d7d01bf88 | ||
|
|
0bc37d2fc2 | ||
|
|
aaa1655af1 | ||
|
|
f1bd4e1bba | ||
|
|
f74147070a | ||
|
|
20f4ea93e4 | ||
|
|
15a28e7c83 | ||
|
|
c550e1de54 | ||
|
|
ab26e561fd | ||
|
|
66968a28a3 | ||
|
|
37d1f91224 | ||
|
|
9e69068d42 | ||
|
|
8e2a9fcd01 | ||
|
|
1753887916 | ||
|
|
21bcffcc87 | ||
|
|
1caed49c75 | ||
|
|
619ea35168 | ||
|
|
877f913e8f | ||
|
|
25c47390c0 | ||
|
|
b3c46348a1 | ||
|
|
6f154194f1 | ||
|
|
c3ab08ce17 | ||
|
|
004fffa992 | ||
|
|
d6bd80c9c0 | ||
|
|
318bcdd011 | ||
|
|
3076f2af68 | ||
|
|
3dd9ef5564 | ||
|
|
367e5fa84e | ||
|
|
97cd61fd13 | ||
|
|
869bf7a345 | ||
|
|
9e15ac242d | ||
|
|
84943e58f1 | ||
|
|
2289bf0a27 | ||
|
|
7fda40c983 | ||
|
|
7eeed8f670 | ||
|
|
4d3f4ed5c2 | ||
|
|
145a4f5c20 | ||
|
|
9afe3d26e9 | ||
|
|
b73a7f1ed8 | ||
|
|
91a2bc3862 | ||
|
|
78a8a840b0 | ||
|
|
f4d54b6ca3 | ||
|
|
bc7a1c332c | ||
|
|
0e75cb9095 | ||
|
|
41b6fb6dcd | ||
|
|
2ca3cbc88f | ||
|
|
d05641a3d6 | ||
|
|
28bf84e05c | ||
|
|
ff51b53660 | ||
|
|
8b8e034b18 | ||
|
|
39927b06e3 | ||
|
|
66db2e7d16 | ||
|
|
a927c33ef1 | ||
|
|
17bc18b881 | ||
|
|
aa643c4a82 | ||
|
|
836fbea676 | ||
|
|
045049243c | ||
|
|
b9813a3494 | ||
|
|
9b42a93ce1 | ||
|
|
8502bceef1 | ||
|
|
663387476f | ||
|
|
daafd83df9 | ||
|
|
f780f2725b | ||
|
|
483aca871a | ||
|
|
352e709c3b | ||
|
|
629057b2c1 | ||
|
|
0e5f53596d | ||
|
|
35c8ea22b1 | ||
|
|
23a548f9b4 | ||
|
|
7169b15fd8 | ||
|
|
f9def8c96f | ||
|
|
ef43837af1 | ||
|
|
0979ca607d | ||
|
|
98fb27f77d | ||
|
|
d68510bbaa | ||
|
|
4177d34b00 | ||
|
|
3ec5c04bf6 | ||
|
|
a877c068b6 | ||
|
|
6a3db90c1e | ||
|
|
a079e0d864 | ||
|
|
719776d66e | ||
|
|
c5af1241e9 | ||
|
|
27e4d7b563 | ||
|
|
450ab34721 | ||
|
|
3e2d4eae2c | ||
|
|
c3542224ae | ||
|
|
429faf44db | ||
|
|
69ad7979ae | ||
|
|
e87caac723 | ||
|
|
494544a4c2 | ||
|
|
94b6118bf7 | ||
|
|
7d6d86c6ff | ||
|
|
bedc327e65 | ||
|
|
40a893a8c9 | ||
|
|
37ebd30a4d |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
|
||||
35
CHANGELOG
35
CHANGELOG
@@ -1,3 +1,38 @@
|
||||
KeePassDX(3.3.0)
|
||||
* Quick search and dynamic filters #163 #462 #521
|
||||
* Keep search context #1141
|
||||
* Add searchable groups #905 #1006
|
||||
* Search with regular expression #175
|
||||
* Merge from file and save as copy #1221 #1204 #840
|
||||
* Fix custom data #1236
|
||||
* Fix education hints #1192
|
||||
* Fix save and app instance in selection mode
|
||||
* New UI and fix styles
|
||||
* Add "Simple" and "Reply" themes
|
||||
|
||||
KeePassDX(3.2.0)
|
||||
* Manage data merge #840 #977
|
||||
* Manage Tags #633
|
||||
* Inherit colors and icon from template #1213 #1130
|
||||
* Entry colors setting #1207
|
||||
* Setting to keep the screen on when watching the entry #1119
|
||||
* Add path in quick search
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(3.1.0)
|
||||
* Add breadcrumb
|
||||
* Add path in search results #1148
|
||||
* Add group info dialog #1177
|
||||
* Manage colors #64 #913
|
||||
* Fix UI in Android 8 #509
|
||||
* Upgrade libs and SDK to 31 #833
|
||||
* Fix parser of database v1 #1201
|
||||
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
||||
|
||||
KeePassDX(3.0.4)
|
||||
* Fix autofill inline bugs #1173 #1165
|
||||
* Small UI change
|
||||
|
||||
KeePassDX(3.0.3)
|
||||
* Change default Argon2 parameters #1098
|
||||
* Add & edit custom icon name #976
|
||||
|
||||
@@ -43,7 +43,7 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
||||
|
||||
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||
* **[Donate](https://www.keepassdx.com/#donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||
|
||||
## Download
|
||||
@@ -72,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 90
|
||||
versionName = "3.0.3"
|
||||
targetSdkVersion 31
|
||||
versionCode = 102
|
||||
versionName = "3.3.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -68,10 +69,14 @@ android {
|
||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||
buildConfigField "String[]", "STYLES_DISABLED",
|
||||
"{\"KeepassDXStyle_Blue\"," +
|
||||
"{\"KeepassDXStyle_Simple\"," +
|
||||
"\"KeepassDXStyle_Simple_Night\"," +
|
||||
"\"KeepassDXStyle_Blue\"," +
|
||||
"\"KeepassDXStyle_Blue_Night\"," +
|
||||
"\"KeepassDXStyle_Red\"," +
|
||||
"\"KeepassDXStyle_Red_Night\"," +
|
||||
"\"KeepassDXStyle_Reply\"," +
|
||||
"\"KeepassDXStyle_Reply_Night\"," +
|
||||
"\"KeepassDXStyle_Purple\"," +
|
||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||
@@ -99,22 +104,26 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.3.0"
|
||||
def room_version = "2.4.1"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "com.android.support:multidex:1.0.3"
|
||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
implementation 'androidx.media:media:1.5.0'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Token auto complete
|
||||
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
@@ -123,7 +132,7 @@ dependencies {
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.10.13'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
// Apache Commons
|
||||
|
||||
@@ -10,15 +10,12 @@
|
||||
android:anyDensity="true" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<!-- Write permission until Android 10 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<!-- Open apps from links -->
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
@@ -30,19 +27,19 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:name="com.kunzisoft.keepass.app.App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/old_backup_rules"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:largeHeap="true"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="n">
|
||||
tools:targetApi="s">
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
@@ -53,7 +50,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -32,8 +32,9 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
@@ -64,18 +65,38 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
||||
// To pass extra inline request
|
||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
|
||||
}
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
||||
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
SearchInfo.getConcreteWebDomain(
|
||||
this,
|
||||
searchInfo.webDomain
|
||||
) { concreteWebDomain ->
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val assistStructure = AutofillHelper
|
||||
.retrieveAutofillComponent(intent)
|
||||
?.assistStructure
|
||||
val newAutofillComponent = if (assistStructure != null) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
compatInlineSuggestionsRequest
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(database, searchInfo)
|
||||
launchSelection(database, newAutofillComponent, searchInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove bundle
|
||||
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||
@@ -95,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
|
||||
private fun launchSelection(database: Database?,
|
||||
autofillComponent: AutofillComponent?,
|
||||
searchInfo: SearchInfo) {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||
|
||||
if (autofillComponent == null) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
@@ -194,34 +213,28 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||
// Wrap into a bundle to bypass the problem
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
searchInfo?.let {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
||||
}
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||
}
|
||||
})
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// TODO Mutable
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
})
|
||||
@@ -234,9 +247,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// TODO Mutable
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
})
|
||||
|
||||
@@ -36,12 +36,20 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
@@ -58,7 +66,10 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.changeControlColor
|
||||
import com.kunzisoft.keepass.view.changeTitleColor
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
@@ -68,15 +79,20 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var appBarLayout: AppBarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var tagsListView: RecyclerView? = null
|
||||
private var tagsAdapter: TagsAdapter? = null
|
||||
private var entryProgress: LinearProgressIndicator? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||
|
||||
private val mEntryActivityEducation = EntryActivityEducation(this)
|
||||
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
@@ -93,7 +109,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
private var mIcon: IconImage? = null
|
||||
private var mIconColor: Int = 0
|
||||
private var mColorAccent: Int = 0
|
||||
private var mControlColor: Int = 0
|
||||
private var mColorPrimary: Int = 0
|
||||
private var mColorBackground: Int = 0
|
||||
private var mBackgroundColor: Int? = null
|
||||
private var mForegroundColor: Int? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -108,8 +129,10 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Get views
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
appBarLayout = findViewById(R.id.app_bar)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
loadingView = findViewById(R.id.loading)
|
||||
@@ -118,10 +141,26 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
collapsingToolbarLayout?.title = " "
|
||||
toolbar?.title = " "
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
// Retrieve the textColor to tint the toolbar
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
||||
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
|
||||
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
|
||||
mControlColor = taControlColor.getColor(0, Color.BLACK)
|
||||
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
|
||||
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||
taColorAccent.recycle()
|
||||
taControlColor.recycle()
|
||||
taColorPrimary.recycle()
|
||||
taColorBackground.recycle()
|
||||
|
||||
// Init Tags adapter
|
||||
tagsAdapter = TagsAdapter(this)
|
||||
tagsListView?.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
adapter = tagsAdapter
|
||||
}
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
@@ -166,10 +205,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim =
|
||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
ColorDrawable(mColorAccent)
|
||||
}
|
||||
|
||||
val entryInfo = entryInfoHistory.entryInfo
|
||||
@@ -184,15 +221,20 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
mUrl = entryInfo.url
|
||||
// Assign tags
|
||||
val tags = entryInfo.tags
|
||||
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||
tagsAdapter?.setTags(tags)
|
||||
// Assign colors
|
||||
val showEntryColors = PreferencesUtil.showEntryColors(this)
|
||||
mBackgroundColor = if (showEntryColors) entryInfo.backgroundColor else null
|
||||
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
@@ -204,9 +246,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||
if (otpElement == null)
|
||||
if (otpElement == null) {
|
||||
entryProgress?.visibility = View.GONE
|
||||
when (otpElement?.type) {
|
||||
} else when (otpElement.type) {
|
||||
// Only add token if HOTP
|
||||
OtpType.HOTP -> {
|
||||
entryProgress?.visibility = View.GONE
|
||||
@@ -215,7 +257,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
OtpType.TOTP -> {
|
||||
entryProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
setProgressCompat(otpElement.secondsRemaining, true)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
@@ -252,13 +294,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
|
||||
// Assign title icon
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -296,6 +331,11 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the screen on
|
||||
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -304,11 +344,33 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun applyToolbarColors() {
|
||||
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
||||
val backgroundDarker = if (mBackgroundColor != null) {
|
||||
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||
} else {
|
||||
mColorBackground
|
||||
}
|
||||
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(
|
||||
iconView,
|
||||
icon,
|
||||
mForegroundColor ?: mColorAccent
|
||||
)
|
||||
}
|
||||
}
|
||||
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
@@ -319,11 +381,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(
|
||||
EntryActivityEducation(
|
||||
this
|
||||
), menu
|
||||
)
|
||||
performedNextEducation(menu)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -335,39 +393,44 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (!mMergeDataAllowed) {
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
applyToolbarColors()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
private fun performedNextEducation(menu: Menu) {
|
||||
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||
as? EntryFragment?
|
||||
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
entryFieldCopyView,
|
||||
{
|
||||
entryFragment.launchEntryCopyEducationAction()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||
// entryEditEducationPerformed
|
||||
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView,
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -375,10 +438,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
@@ -415,6 +474,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
R.id.menu_save_database -> {
|
||||
saveDatabase()
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
reloadDatabase()
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.*
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
@@ -103,6 +104,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var mEntryLoaded: Boolean = false
|
||||
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
|
||||
|
||||
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
|
||||
|
||||
private var mAllowCustomFields = false
|
||||
private var mAllowOTP = false
|
||||
|
||||
@@ -110,7 +113,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
private var mEntryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
|
||||
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||
mEntryEditViewModel.selectIcon(icon)
|
||||
@@ -180,8 +183,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
|
||||
// Lock button
|
||||
lockView?.setOnClickListener { lockAndExit() }
|
||||
@@ -243,6 +244,15 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
|
||||
ColorPickerDialogFragment.newInstance(color)
|
||||
.show(supportFragmentManager, "ColorPickerFragment")
|
||||
}
|
||||
|
||||
mColorPickerViewModel.colorPicked.observe(this) { color ->
|
||||
mEntryEditViewModel.selectColor(color)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||
if (dateInstant.type == DateInstant.Type.TIME) {
|
||||
// Launch the time picker
|
||||
@@ -526,10 +536,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(it)
|
||||
}
|
||||
performedNextEducation()
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -556,19 +564,19 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
private fun performedNextEducation() {
|
||||
|
||||
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
|
||||
as? EntryEditFragment?
|
||||
val generatePasswordView = entryEditFragment?.getActionImageView()
|
||||
val generatePasswordEductionPerformed = generatePasswordView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
&& mEntryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
generatePasswordView,
|
||||
{
|
||||
entryEditFragment.launchGeneratePasswordEductionAction()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
performedNextEducation()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -577,33 +585,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
val addNewFieldEducationPerformed = mAllowCustomFields
|
||||
&& addNewFieldView != null
|
||||
&& addNewFieldView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
&& mEntryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
addNewCustomField()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
performedNextEducation()
|
||||
}
|
||||
)
|
||||
if (!addNewFieldEducationPerformed) {
|
||||
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||
val addAttachmentEducationPerformed = attachmentView != null
|
||||
&& attachmentView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||
&& mEntryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||
attachmentView,
|
||||
{
|
||||
addNewAttachment()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
performedNextEducation()
|
||||
}
|
||||
)
|
||||
if (!addAttachmentEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null
|
||||
&& setupOtpView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
&& mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
{
|
||||
setupOtp()
|
||||
@@ -650,8 +658,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
override fun acceptPassword(passwordField: Field) {
|
||||
mEntryEditViewModel.selectPassword(passwordField)
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
||||
|
||||
// Views
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
@@ -78,6 +78,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||
|
||||
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
|
||||
|
||||
// Adapter to manage database history list
|
||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||
|
||||
@@ -124,7 +126,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
SetMainCredentialDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
@@ -132,7 +134,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
// History list
|
||||
@@ -291,7 +293,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
PasswordActivity.launch(this,
|
||||
MainCredentialActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
{ exception ->
|
||||
@@ -392,39 +394,40 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||
private fun performedNextEducation() {
|
||||
// If no recent files
|
||||
val createDatabaseEducationPerformed =
|
||||
createDatabaseButtonView != null
|
||||
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||
&& mAdapterDatabaseHistory != null
|
||||
&& mAdapterDatabaseHistory!!.itemCount == 0
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
&& mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
createDatabaseButtonView!!,
|
||||
{
|
||||
createNewFile()
|
||||
},
|
||||
{
|
||||
// But if the user cancel, it can also select a database
|
||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||
performedNextEducation()
|
||||
})
|
||||
if (!createDatabaseEducationPerformed) {
|
||||
// selectDatabaseEducationPerformed
|
||||
openDatabaseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
&& mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
openDatabaseButtonView!!,
|
||||
{ tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mExternalFileHelper?.openDocument()
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
{
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,35 +21,34 @@ package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.KeyEvent.KEYCODE_ENTER
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.*
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
@@ -57,11 +56,9 @@ import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||
@@ -70,23 +67,20 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
private var confirmButtonView: Button? = null
|
||||
private var checkboxPasswordView: CompoundButton? = null
|
||||
private var checkboxKeyFileView: CompoundButton? = null
|
||||
private var infoContainerView: ViewGroup? = null
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||
@@ -94,25 +88,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||
|
||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||
|
||||
private var mDefaultDatabase: Boolean = false
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
private var mDatabaseKeyFileUri: Uri? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
infoContainerView?.visibility = if (value) {
|
||||
mReadOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
@@ -122,7 +107,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
setContentView(R.layout.activity_main_credential)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar?.title = getString(R.string.app_name)
|
||||
@@ -130,16 +115,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||
|
||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||
} else {
|
||||
@@ -147,41 +128,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
}
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
})
|
||||
passwordView?.setOnKeyListener { _, _, keyEvent ->
|
||||
var handled = false
|
||||
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
||||
&& keyEvent?.keyCode == KEYCODE_ENTER) {
|
||||
verifyCheckboxesAndLoadDatabase()
|
||||
handled = true
|
||||
}
|
||||
handled
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
mainCredentialView?.onValidateListener = {
|
||||
loadDatabase()
|
||||
}
|
||||
|
||||
// If is a view intent
|
||||
getUriFromIntent(intent)
|
||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||
}
|
||||
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment = supportFragmentManager
|
||||
@@ -196,9 +155,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
}
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||
mainCredentialView?.onPasswordChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableOrNotTheConfirmationButton()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
@@ -208,19 +168,29 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
|
||||
// Observe database file change
|
||||
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
|
||||
// Force read only if the file does not exists
|
||||
mForceReadOnly = databaseFile?.let {
|
||||
val databaseFileNotExists = databaseFile?.let {
|
||||
!it.databaseFileExists
|
||||
} ?: true
|
||||
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||
mReadOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
mForceReadOnly = databaseFileNotExists
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Post init uri with KeyFile only if needed
|
||||
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
|
||||
val keyFileUri =
|
||||
if (mRememberKeyFile
|
||||
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||
&& (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
|
||||
databaseFile?.keyFileUri
|
||||
} else {
|
||||
mDatabaseKeyFileUri
|
||||
databaseKeyFileUri
|
||||
}
|
||||
|
||||
// Define title
|
||||
@@ -233,10 +203,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity)
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) {
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
@@ -249,8 +219,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
checkPermission()
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
@@ -277,7 +245,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
} else {
|
||||
passwordView?.requestFocusFromTouch()
|
||||
mainCredentialView?.requestPasswordFocus()
|
||||
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
@@ -294,7 +262,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEntity: CipherDatabaseEntity? = null
|
||||
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
@@ -302,8 +270,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||
?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEntity =
|
||||
resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||
cipherEncryptDatabase =
|
||||
resultData.getParcelable(CIPHER_DATABASE_KEY)
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
@@ -311,7 +279,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEntity,
|
||||
cipherEncryptDatabase,
|
||||
true
|
||||
)
|
||||
}
|
||||
@@ -347,11 +315,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
||||
mainCredentialView?.populateKeyFileTextView(it)
|
||||
}
|
||||
}
|
||||
try {
|
||||
intent?.removeExtra(KEY_KEYFILE)
|
||||
} catch (e: Exception) {}
|
||||
mDatabaseFileUri?.let {
|
||||
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
@@ -386,51 +359,68 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun retrieveCredentialForEncryption(): String {
|
||||
return passwordView?.text?.toString() ?: ""
|
||||
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||
?: byteArrayOf()
|
||||
}
|
||||
|
||||
override fun conditionToStoreCredential(): Boolean {
|
||||
return checkboxPasswordView?.isChecked == true
|
||||
return mainCredentialView?.conditionToStoreCredential() == true
|
||||
}
|
||||
|
||||
override fun onCredentialEncrypted(databaseUri: Uri,
|
||||
encryptedCredential: String,
|
||||
ivSpec: String) {
|
||||
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
// Load the database if password is registered with biometric
|
||||
verifyCheckboxesAndLoadDatabase(
|
||||
CipherDatabaseEntity(
|
||||
databaseUri.toString(),
|
||||
encryptedCredential,
|
||||
ivSpec)
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredentialView?.getMainCredential(),
|
||||
cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCredentialDecrypted(databaseUri: Uri,
|
||||
decryptedCredential: String) {
|
||||
// Load the database if password is retrieve from biometric
|
||||
// Retrieve from biometric
|
||||
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
|
||||
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||
override fun passwordToStore(password: String?): ByteArray? {
|
||||
return password?.toByteArray()
|
||||
}
|
||||
|
||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (actionId == IME_ACTION_DONE) {
|
||||
verifyCheckboxesAndLoadDatabase()
|
||||
return true
|
||||
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
|
||||
// TODO create byte array to store keyfile
|
||||
return null
|
||||
}
|
||||
return false
|
||||
|
||||
override fun hardwareKeyToStore(): ByteArray? {
|
||||
// TODO create byte array to store hardware key
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
// Load the database if password is retrieve from biometric
|
||||
// Retrieve from biometric
|
||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
when (cipherDecryptDatabase.credentialStorage) {
|
||||
CredentialStorage.PASSWORD -> {
|
||||
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
|
||||
}
|
||||
CredentialStorage.KEY_FILE -> {
|
||||
// TODO advanced unlock key file
|
||||
}
|
||||
CredentialStorage.HARDWARE_KEY -> {
|
||||
// TODO advanced unlock hardware key
|
||||
}
|
||||
}
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredential,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
// Define Key File text
|
||||
if (mRememberKeyFile) {
|
||||
populateKeyFileTextView(keyFileUri)
|
||||
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define listener for validate button
|
||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||
confirmButtonView?.setOnClickListener { loadDatabase() }
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
@@ -439,66 +429,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
populatePasswordTextView(password)
|
||||
mainCredentialView?.populatePasswordTextView(password)
|
||||
}
|
||||
if (launchImmediately) {
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
loadDatabase()
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
enableConfirmationButton()
|
||||
|
||||
// Auto select the password field and open keyboard
|
||||
passwordView?.postDelayed({
|
||||
passwordView?.requestFocusFromTouch()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
|
||||
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 100)
|
||||
mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
|
||||
}
|
||||
|
||||
private fun enableOrNotTheConfirmationButton() {
|
||||
private fun enableConfirmationButton() {
|
||||
// Enable or not the open button if setting is checked
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||
checkboxPasswordView?.let {
|
||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
||||
|| checkboxKeyFileView?.isChecked == true)
|
||||
}
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
|
||||
confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
|
||||
} else {
|
||||
confirmButtonView?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
populatePasswordTextView(null)
|
||||
mainCredentialView?.populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
mDatabaseKeyFileUri = null
|
||||
populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populatePasswordTextView(text: String?) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
passwordView?.setText("")
|
||||
if (checkboxPasswordView?.isChecked == true)
|
||||
checkboxPasswordView?.isChecked = false
|
||||
} else {
|
||||
passwordView?.setText(text)
|
||||
if (checkboxPasswordView?.isChecked != true)
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateKeyFileTextView(uri: Uri?) {
|
||||
if (uri == null || uri.toString().isEmpty()) {
|
||||
keyFileSelectionView?.uri = null
|
||||
if (checkboxKeyFileView?.isChecked == true)
|
||||
checkboxKeyFileView?.isChecked = false
|
||||
} else {
|
||||
keyFileSelectionView?.uri = uri
|
||||
if (checkboxKeyFileView?.isChecked != true)
|
||||
checkboxKeyFileView?.isChecked = true
|
||||
mainCredentialView?.populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,42 +467,20 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||
mDatabaseKeyFileUri?.let {
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val password: String? = passwordView?.text?.toString()
|
||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
||||
keyFile: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
private fun loadDatabase() {
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredentialView?.getMainCredential(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadDatabase(databaseFileUri: Uri?,
|
||||
password: String?,
|
||||
keyFileUri: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
mainCredential: MainCredential?,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?) {
|
||||
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
clearCredentialsViews()
|
||||
@@ -564,10 +499,11 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
// Show the progress dialog and load the database
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseUri,
|
||||
MainCredential(password, keyFileUri),
|
||||
mainCredential ?: MainCredential(),
|
||||
mReadOnly,
|
||||
cipherDatabaseEntity,
|
||||
false)
|
||||
cipherEncryptDatabase,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,13 +511,13 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
loadDatabase(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
cipherEncryptDatabase,
|
||||
fixDuplicateUUID
|
||||
)
|
||||
}
|
||||
@@ -613,61 +549,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
return true
|
||||
}
|
||||
|
||||
// Check permission
|
||||
private fun checkPermission() {
|
||||
if (Build.VERSION.SDK_INT in 23..28
|
||||
&& !mReadOnly
|
||||
&& !mPermissionAsked) {
|
||||
mPermissionAsked = true
|
||||
// Check self permission to show or not the dialog
|
||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
val permissions = arrayOf(writePermission)
|
||||
if (toolbar != null
|
||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu) {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(menu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu) {
|
||||
private fun performedNextEducation(menu: Menu) {
|
||||
val educationToolbar = toolbar
|
||||
val unlockEducationPerformed = educationToolbar != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
&& mPasswordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
educationToolbar,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
val readOnlyEducationPerformed =
|
||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
&& mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
try {
|
||||
@@ -675,19 +583,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to find read mode menu")
|
||||
}
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
|
||||
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
||||
advancedUnlockFragment?.performEducation(mPasswordActivityEducation,
|
||||
readOnlyEducationPerformed,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -718,7 +626,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = PasswordActivity::class.java.name
|
||||
private val TAG = MainCredentialActivity::class.java.name
|
||||
|
||||
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||
|
||||
@@ -729,12 +637,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, PasswordActivity::class.java)
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
@@ -870,30 +776,30 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
PasswordActivity.launch(activity,
|
||||
MainCredentialActivity.launch(activity,
|
||||
databaseUri, keyFile)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
PasswordActivity.launchForSearchResult(activity,
|
||||
MainCredentialActivity.launchForSearchResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
PasswordActivity.launchForSaveResult(activity,
|
||||
MainCredentialActivity.launchForSaveResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
PasswordActivity.launchForKeyboardResult(activity,
|
||||
MainCredentialActivity.launchForKeyboardResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PasswordActivity.launchForAutofillResult(activity,
|
||||
MainCredentialActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
@@ -904,7 +810,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
PasswordActivity.launchForRegistration(activity,
|
||||
MainCredentialActivity.launchForRegistration(activity,
|
||||
databaseUri, keyFile,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
|
||||
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
||||
|
||||
private lateinit var enableSwitchView: CompoundButton
|
||||
private lateinit var chromaColorView: ChromaColorView
|
||||
|
||||
private var mDefaultColor = Color.WHITE
|
||||
private var mActivated = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||
enableSwitchView = root.findViewById(R.id.switch_element)
|
||||
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
}
|
||||
}
|
||||
enableSwitchView.isChecked = mActivated
|
||||
chromaColorView.currentColor = mDefaultColor
|
||||
|
||||
chromaColorView.setOnColorChangedListener {
|
||||
if (!enableSwitchView.isChecked)
|
||||
enableSwitchView.isChecked = true
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val color: Int? = if (enableSwitchView.isChecked)
|
||||
chromaColorView.currentColor
|
||||
else
|
||||
null
|
||||
mColorPickerViewModel.pickColor(color)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||
|
||||
fun newInstance(
|
||||
@ColorInt initialColor: Int?,
|
||||
): ColorPickerDialogFragment {
|
||||
return ColorPickerDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
||||
putBoolean(ARG_ACTIVATED, initialColor != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||
|
||||
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private var mGroupInfo = GroupInfo()
|
||||
|
||||
private lateinit var iconView: ImageView
|
||||
private var mIconColor: Int = 0
|
||||
private lateinit var nameTextView: TextView
|
||||
private lateinit var tagsListView: RecyclerView
|
||||
private var tagsAdapter: TagsAdapter? = null
|
||||
private lateinit var notesTextLabelView: TextView
|
||||
private lateinit var notesTextView: TextView
|
||||
private lateinit var expirationView: DateTimeFieldView
|
||||
private lateinit var creationView: TextView
|
||||
private lateinit var modificationView: TextView
|
||||
private lateinit var searchableLabelView: TextView
|
||||
private lateinit var searchableView: TextView
|
||||
private lateinit var autoTypeLabelView: TextView
|
||||
private lateinit var autoTypeView: TextView
|
||||
private lateinit var uuidContainerView: ViewGroup
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||
|
||||
if (database?.allowCustomSearchableGroup() == true) {
|
||||
searchableLabelView.visibility = View.VISIBLE
|
||||
searchableView.visibility = View.VISIBLE
|
||||
} else {
|
||||
searchableLabelView.visibility = View.GONE
|
||||
searchableView.visibility = View.GONE
|
||||
}
|
||||
|
||||
// TODO Auto-Type
|
||||
/*
|
||||
if (database?.allowAutoType() == true) {
|
||||
autoTypeLabelView.visibility = View.VISIBLE
|
||||
autoTypeView.visibility = View.VISIBLE
|
||||
} else {
|
||||
autoTypeLabelView.visibility = View.GONE
|
||||
autoTypeView.visibility = View.GONE
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
||||
iconView = root.findViewById(R.id.group_icon)
|
||||
nameTextView = root.findViewById(R.id.group_name)
|
||||
tagsListView = root.findViewById(R.id.group_tags_list_view)
|
||||
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
||||
notesTextView = root.findViewById(R.id.group_note)
|
||||
expirationView = root.findViewById(R.id.group_expiration)
|
||||
creationView = root.findViewById(R.id.group_created)
|
||||
modificationView = root.findViewById(R.id.group_modified)
|
||||
searchableLabelView = root.findViewById(R.id.group_searchable_label)
|
||||
searchableView = root.findViewById(R.id.group_searchable)
|
||||
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
|
||||
autoTypeView = root.findViewById(R.id.group_auto_type)
|
||||
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
||||
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
mIconColor = ta.getColor(0, Color.WHITE)
|
||||
ta.recycle()
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate info in views
|
||||
val title = mGroupInfo.title
|
||||
if (title.isEmpty()) {
|
||||
nameTextView.visibility = View.GONE
|
||||
} else {
|
||||
nameTextView.text = title
|
||||
nameTextView.visibility = View.VISIBLE
|
||||
}
|
||||
tagsAdapter = TagsAdapter(activity)
|
||||
tagsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
adapter = tagsAdapter
|
||||
}
|
||||
val tags = mGroupInfo.tags
|
||||
tagsListView.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||
tagsAdapter?.setTags(tags)
|
||||
val notes = mGroupInfo.notes
|
||||
if (notes == null || notes.isEmpty()) {
|
||||
notesTextLabelView.visibility = View.GONE
|
||||
notesTextView.visibility = View.GONE
|
||||
} else {
|
||||
notesTextView.text = notes
|
||||
notesTextLabelView.visibility = View.VISIBLE
|
||||
notesTextView.visibility = View.VISIBLE
|
||||
}
|
||||
expirationView.activation = mGroupInfo.expires
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
||||
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||
mGroupInfo.defaultAutoTypeSequence)
|
||||
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
||||
if (uuid == null || uuid.isEmpty()) {
|
||||
uuidContainerView.visibility = View.GONE
|
||||
} else {
|
||||
uuidReferenceView.text = uuid
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
|
||||
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
|
||||
return when {
|
||||
enable == null -> getString(R.string.inherited) + valueString
|
||||
enable -> getString(R.string.enable) + valueString
|
||||
else -> getString(R.string.disable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
data class Error(val isError: Boolean, val messageId: Int?)
|
||||
|
||||
companion object {
|
||||
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||
val fragment = GroupDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,20 +23,23 @@ import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
||||
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||
import com.kunzisoft.keepass.view.InheritedCompletionView
|
||||
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||
import com.tokenautocomplete.FilteredArrayAdapter
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
@@ -55,6 +58,14 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
private lateinit var notesTextLayoutView: TextInputLayout
|
||||
private lateinit var notesTextView: TextView
|
||||
private lateinit var expirationView: DateTimeEditFieldView
|
||||
private lateinit var searchableContainerView: TextInputLayout
|
||||
private lateinit var searchableView: InheritedCompletionView
|
||||
private lateinit var autoTypeContainerView: ViewGroup
|
||||
private lateinit var autoTypeInheritedView: InheritedCompletionView
|
||||
private lateinit var autoTypeSequenceView: TextView
|
||||
private lateinit var tagsContainerView: TextInputLayout
|
||||
private lateinit var tagsCompletionView: TagsCompletionView
|
||||
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||
|
||||
enum class EditGroupDialogAction {
|
||||
CREATION, UPDATE, NONE;
|
||||
@@ -107,10 +118,30 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
|
||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (database?.allowAutoType() == true) {
|
||||
autoTypeContainerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
autoTypeContainerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -122,6 +153,13 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
|
||||
searchableView = root.findViewById(R.id.group_edit_searchable)
|
||||
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
|
||||
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
|
||||
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
|
||||
tagsContainerView = root.findViewById(R.id.group_tags_label)
|
||||
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
@@ -197,6 +235,19 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
expirationView.activation = groupInfo.expires
|
||||
expirationView.dateTime = groupInfo.expiryTime
|
||||
|
||||
// Set searchable
|
||||
searchableView.setValue(groupInfo.searchable)
|
||||
// Set auto-type
|
||||
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
|
||||
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
|
||||
// Set Tags
|
||||
groupInfo.tags.let { tags ->
|
||||
tagsCompletionView.setText("")
|
||||
for (i in 0 until tags.size()) {
|
||||
tagsCompletionView.addObjectSync(tags.get(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveGroupInfoFromViews() {
|
||||
@@ -208,6 +259,10 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
mGroupInfo.expires = expirationView.activation
|
||||
mGroupInfo.expiryTime = expirationView.dateTime
|
||||
mGroupInfo.searchable = searchableView.getValue()
|
||||
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
|
||||
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
|
||||
mGroupInfo.tags = tagsCompletionView.getTags()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -246,8 +301,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
companion object {
|
||||
|
||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
|
||||
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
|
||||
private var mListener: AskMainCredentialDialogListener? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
interface AskMainCredentialDialogListener {
|
||||
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AskMainCredentialDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
|
||||
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
||||
databaseUri?.let {
|
||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||
UriUtil.getFileData(requireContext(), it)?.name
|
||||
}
|
||||
builder.setView(root)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogPositiveClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogNegativeClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun retrieveMainCredential(): MainCredential {
|
||||
return mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
|
||||
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
|
||||
|
||||
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
|
||||
val fragment = MainCredentialDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFile: Uri? = null
|
||||
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
private var mListener: AssignMainCredentialDialogListener? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
@@ -74,7 +74,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
interface AssignPasswordDialogListener {
|
||||
interface AssignMainCredentialDialogListener {
|
||||
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||
}
|
||||
@@ -82,10 +82,10 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AssignPasswordDialogListener
|
||||
mListener = activity as AssignMainCredentialDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
||||
+ " must implement " + AssignMainCredentialDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||
rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
@@ -254,7 +254,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
@@ -269,7 +269,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
builder.setMessage(R.string.warning_no_encryption_key)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mNoKeyConfirmationDialog = builder.create()
|
||||
@@ -301,8 +301,8 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
|
||||
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
||||
val fragment = AssignMasterKeyDialogFragment()
|
||||
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
||||
val fragment = SetMainCredentialDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||
fragment.arguments = args
|
||||
@@ -29,10 +29,12 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
@@ -40,11 +42,10 @@ import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.view.TemplateEditView
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.view.*
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import com.tokenautocomplete.FilteredArrayAdapter
|
||||
|
||||
|
||||
class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
@@ -55,6 +56,9 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
private lateinit var attachmentsContainerView: ViewGroup
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
private lateinit var tagsContainerView: TextInputLayout
|
||||
private lateinit var tagsCompletionView: TagsCompletionView
|
||||
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||
|
||||
private var mTemplate: Template? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
@@ -87,6 +91,8 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
templateView = view.findViewById(R.id.template_view)
|
||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||
tagsContainerView = view.findViewById(R.id.entry_tags_label)
|
||||
tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view)
|
||||
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||
attachmentsListView.apply {
|
||||
@@ -99,6 +105,12 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
setOnIconClickListener {
|
||||
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||
}
|
||||
setOnBackgroundColorClickListener {
|
||||
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||
}
|
||||
setOnForegroundColorClickListener {
|
||||
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||
}
|
||||
setOnCustomEditionActionClickListener { field ->
|
||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||
}
|
||||
@@ -140,13 +152,22 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
|
||||
val entryInfo = retrieveEntryInfo()
|
||||
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, entryInfo)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
||||
templateView.setIcon(iconImage)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||
templateView.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||
templateView.setForegroundColor(color)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||
templateView.setPasswordField(passwordField)
|
||||
}
|
||||
@@ -263,18 +284,34 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
attachmentsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
// Populate entry views
|
||||
templateView.setEntryInfo(entryInfo)
|
||||
|
||||
// Set Tags
|
||||
entryInfo?.tags?.let { tags ->
|
||||
tagsCompletionView.setText("")
|
||||
for (i in 0 until tags.size()) {
|
||||
tagsCompletionView.addObjectSync(tags.get(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Manage attachments
|
||||
setAttachments(entryInfo?.attachments ?: listOf())
|
||||
}
|
||||
|
||||
private fun retrieveEntryInfo(): EntryInfo {
|
||||
val entryInfo = templateView.getEntryInfo()
|
||||
entryInfo.tags = tagsCompletionView.getTags()
|
||||
entryInfo.attachments = getAttachments().toMutableList()
|
||||
return entryInfo
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ class EntryFragment: DatabaseFragment() {
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private lateinit var customDataView: TextView
|
||||
|
||||
private lateinit var uuidContainerView: View
|
||||
private lateinit var uuidView: TextView
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
private var mClipboardHelper: ClipboardHelper? = null
|
||||
@@ -84,11 +85,13 @@ class EntryFragment: DatabaseFragment() {
|
||||
creationDateView = view.findViewById(R.id.entry_created)
|
||||
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||
|
||||
// TODO Custom data
|
||||
// customDataView = view.findViewById(R.id.entry_custom_data)
|
||||
|
||||
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
uuidView = view.findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||
@@ -156,11 +159,14 @@ class EntryFragment: DatabaseFragment() {
|
||||
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||
|
||||
// Assign dates
|
||||
assignCreationDate(entryInfo?.creationTime)
|
||||
assignModificationDate(entryInfo?.lastModificationTime)
|
||||
creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
|
||||
modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
|
||||
|
||||
// TODO Custom data
|
||||
// customDataView.text = entryInfo?.customData?.toString()
|
||||
|
||||
// Assign special data
|
||||
assignUUID(entryInfo?.id)
|
||||
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
||||
}
|
||||
|
||||
private fun showClipboardDialog() {
|
||||
@@ -191,19 +197,6 @@ class EntryFragment: DatabaseFragment() {
|
||||
templateView.reload()
|
||||
}
|
||||
|
||||
private fun assignCreationDate(date: DateInstant?) {
|
||||
creationDateView.text = date?.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
private fun assignModificationDate(date: DateInstant?) {
|
||||
modificationDateView.text = date?.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
private fun assignUUID(uuid: UUID?) {
|
||||
uuidView.text = uuid?.toString()
|
||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
@@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
private var groupRefreshed: GroupRefreshedListener? = null
|
||||
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
private var mLayoutManager: LinearLayoutManager? = null
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
private var mAdapter: NodesAdapter? = null
|
||||
|
||||
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||
|
||||
@@ -102,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
// TODO Change to ViewModel
|
||||
try {
|
||||
nodeClickListener = context as NodeClickListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -115,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
} catch (e: ClassCastException) {
|
||||
onScrollListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
Log.w(
|
||||
TAG, context.toString()
|
||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
groupRefreshed = context as GroupRefreshedListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupRefreshedListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
nodeClickListener = null
|
||||
onScrollListener = null
|
||||
groupRefreshed = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@@ -138,10 +149,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
contextThemed?.let { context ->
|
||||
database?.let { database ->
|
||||
mAdapter = NodeAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: Database, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (mCurrentGroup?.isVirtual == false
|
||||
&& nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
@@ -158,7 +170,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
if (mCurrentGroup?.isVirtual == false
|
||||
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
@@ -195,7 +208,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
// To apply theme
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_group, container, false)
|
||||
.inflate(R.layout.fragment_nodes, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -246,9 +259,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
private fun rebuildList() {
|
||||
try {
|
||||
// Add elements to the list
|
||||
mCurrentGroup?.let { mainGroup ->
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
// Thrown an exception when sort cannot be performed
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
mAdapter?.rebuildList(currentGroup)
|
||||
}
|
||||
} catch (e:Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list", e)
|
||||
@@ -260,6 +273,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
} else {
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
groupRefreshed?.onGroupRefreshed()
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
@@ -295,12 +310,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context))
|
||||
PreferencesUtil.getGroupsBeforeSort(context)
|
||||
)
|
||||
}
|
||||
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
@@ -447,6 +464,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
fun onScrolled(dy: Int)
|
||||
}
|
||||
|
||||
interface GroupRefreshedListener {
|
||||
fun onGroupRefreshed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = GroupFragment::class.java.name
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
@@ -59,9 +59,9 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
fun loadDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEntity: CipherDatabaseEntity?,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean) {
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid)
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
||||
}
|
||||
|
||||
protected fun closeDatabase() {
|
||||
|
||||
@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
protected var mDatabaseReadOnly: Boolean = true
|
||||
protected var mMergeDataAllowed: Boolean = false
|
||||
private var mAutoSaveEnable: Boolean = true
|
||||
|
||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||
@@ -87,9 +88,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveName.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
@@ -197,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
mIconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
checkRegister()
|
||||
@@ -212,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Reload the current activity
|
||||
if (result.isSuccess) {
|
||||
@@ -254,9 +263,23 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||
}
|
||||
|
||||
fun saveDatabaseTo(uri: Uri) {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
}
|
||||
|
||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun createEntry(newEntry: Entry,
|
||||
parent: Group) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -11,6 +13,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.SpecialModeView
|
||||
|
||||
|
||||
/**
|
||||
* Activity to manage database special mode (ie: selection mode)
|
||||
*/
|
||||
@@ -63,8 +66,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,8 +79,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,11 +89,19 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun backToTheMainAppAndFinish() {
|
||||
// To move the app in background and return to the main app
|
||||
moveTaskToBack(true)
|
||||
// To remove this instance in the OS app selector
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
finish()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -160,12 +169,17 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
}
|
||||
|
||||
// To hide home button from the regular toolbar in special mode
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
if (mSpecialMode != SpecialMode.DEFAULT
|
||||
&& hideHomeButtonIfModeIsNotDefault()) {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
open fun hideHomeButtonIfModeIsNotDefault(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun blockAutofill(searchInfo: SearchInfo?) {
|
||||
val webDomain = searchInfo?.webDomain
|
||||
val applicationId = searchInfo?.applicationId
|
||||
|
||||
@@ -69,8 +69,10 @@ object Stylish {
|
||||
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
||||
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
||||
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
||||
context.getString(R.string.list_style_name_simple_night) -> context.getString(R.string.list_style_name_simple)
|
||||
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
|
||||
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
|
||||
context.getString(R.string.list_style_name_reply_night) -> context.getString(R.string.list_style_name_reply)
|
||||
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
|
||||
else -> styleString
|
||||
}
|
||||
@@ -81,8 +83,10 @@ object Stylish {
|
||||
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
||||
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
||||
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
||||
context.getString(R.string.list_style_name_simple) -> context.getString(R.string.list_style_name_simple_night)
|
||||
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
|
||||
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
|
||||
context.getString(R.string.list_style_name_reply) -> context.getString(R.string.list_style_name_reply_night)
|
||||
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
|
||||
else -> styleString
|
||||
}
|
||||
@@ -113,10 +117,14 @@ object Stylish {
|
||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
|
||||
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
||||
context.getString(R.string.list_style_name_simple) -> R.style.KeepassDXStyle_Simple
|
||||
context.getString(R.string.list_style_name_simple_night) -> R.style.KeepassDXStyle_Simple_Night
|
||||
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
||||
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
|
||||
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
||||
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
|
||||
context.getString(R.string.list_style_name_reply) -> R.style.KeepassDXStyle_Reply
|
||||
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
|
||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||
else -> R.style.KeepassDXStyle_Light
|
||||
|
||||
@@ -28,7 +28,7 @@ import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
|
||||
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
||||
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
|
||||
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
||||
|| DATABASE_PREFERENCE_CHANGED) {
|
||||
DATABASE_PREFERENCE_CHANGED = false
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
recreateActivity()
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class StylishFragment : Fragment() {
|
||||
@@ -47,27 +49,41 @@ abstract class StylishFragment : Fragment() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = requireActivity().window
|
||||
val defaultColor = Color.BLACK
|
||||
|
||||
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
|
||||
try {
|
||||
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||
taStatusBarColor?.recycle()
|
||||
} catch (e: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
}
|
||||
windowInset.isAppearanceLightStatusBars = taWindowStatusLight
|
||||
?.getBoolean(0, false) == true
|
||||
taWindowStatusLight?.recycle()
|
||||
} catch (e: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
|
||||
}
|
||||
}
|
||||
try {
|
||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||
taNavigationBarColor?.recycle()
|
||||
} catch (e: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
try {
|
||||
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
|
||||
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
|
||||
?.getBoolean(0, false) == true
|
||||
taWindowLightNavigationBar?.recycle()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
@@ -76,4 +92,8 @@ abstract class StylishFragment : Fragment() {
|
||||
contextThemed = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = StylishFragment::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
|
||||
class BreadcrumbAdapter(val context: Context)
|
||||
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
|
||||
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||
|
||||
private var mShowNumberEntries = false
|
||||
private var mShowUUID = false
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
init {
|
||||
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
mShowUUID = PreferencesUtil.showUUID(context)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setNode(node: Node?) {
|
||||
mNodeBreadcrumb.clear()
|
||||
node?.let {
|
||||
var currentNode = it
|
||||
mNodeBreadcrumb.add(0, currentNode)
|
||||
while (currentNode.containsParent()) {
|
||||
currentNode.parent?.let { parent ->
|
||||
currentNode = parent
|
||||
mNodeBreadcrumb.add(0, currentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (position) {
|
||||
mNodeBreadcrumb.size - 1 -> 0
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
||||
return BreadcrumbGroupViewHolder(inflater.inflate(
|
||||
when (viewType) {
|
||||
0 -> R.layout.item_group
|
||||
else -> R.layout.item_breadcrumb
|
||||
}, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
|
||||
val node = mNodeBreadcrumb[position]
|
||||
|
||||
holder.groupNameView.apply {
|
||||
text = node?.title ?: ""
|
||||
strikeOut(node?.isCurrentlyExpires ?: false)
|
||||
}
|
||||
|
||||
holder.itemView.apply {
|
||||
setOnClickListener {
|
||||
node?.let {
|
||||
onItemClickListener?.invoke(it, position)
|
||||
}
|
||||
}
|
||||
setOnLongClickListener {
|
||||
node?.let {
|
||||
onLongItemClickListener?.invoke(it, position)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
if (node?.type == Type.GROUP) {
|
||||
(node as Group).let { group ->
|
||||
|
||||
holder.groupIconView?.let { imageView ->
|
||||
iconDrawableFactory?.assignDatabaseIcon(
|
||||
imageView,
|
||||
group.icon,
|
||||
mIconColor
|
||||
)
|
||||
}
|
||||
|
||||
holder.groupNumbersView?.apply {
|
||||
if (mShowNumberEntries) {
|
||||
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
||||
text = group.numberOfChildEntries.toString()
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
holder.groupMetaView?.apply {
|
||||
val meta = group.nodeId.toVisualString()
|
||||
visibility = if (meta != null
|
||||
&& !group.isVirtual
|
||||
&& mShowUUID
|
||||
) {
|
||||
text = meta
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mNodeBreadcrumb.size
|
||||
}
|
||||
|
||||
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
|
||||
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
|
||||
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
|
||||
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,13 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
@@ -55,9 +54,9 @@ import java.util.*
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
class NodeAdapter (private val context: Context,
|
||||
class NodesAdapter (private val context: Context,
|
||||
private val database: Database)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||
|
||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||
@@ -74,22 +73,29 @@ class NodeAdapter (private val context: Context,
|
||||
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||
private var mIconDefaultDimension: Float = 0F
|
||||
|
||||
private var mShowEntryColors: Boolean = true
|
||||
private var mShowUserNames: Boolean = true
|
||||
private var mShowNumberEntries: Boolean = true
|
||||
private var mShowOTP: Boolean = false
|
||||
private var mShowUUID: Boolean = false
|
||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||
private var mOldVirtualGroup = false
|
||||
private var mVirtualGroup = false
|
||||
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
private var mClipboardHelper = ClipboardHelper(context)
|
||||
|
||||
@ColorInt
|
||||
private val mContentSelectionColor: Int
|
||||
private val mTextColorPrimary: Int
|
||||
@ColorInt
|
||||
private val mIconGroupColor: Int
|
||||
private val mTextColor: Int
|
||||
@ColorInt
|
||||
private val mIconEntryColor: Int
|
||||
private val mTextColorSecondary: Int
|
||||
@ColorInt
|
||||
private val mColorAccentLight: Int
|
||||
@ColorInt
|
||||
private val mColorOnAccentColor: Int
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
@@ -106,16 +112,26 @@ class NodeAdapter (private val context: Context,
|
||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||
|
||||
// Color of content selection
|
||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||
// Retrieve the color to tint the icon
|
||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
taTextColorPrimary.recycle()
|
||||
// In two times to fix bug compilation
|
||||
// To get text color
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
// To get text color secondary
|
||||
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
||||
taTextColorSecondary.recycle()
|
||||
// To get background color for selection
|
||||
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
|
||||
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
|
||||
taColorAccentLight.recycle()
|
||||
// To get text color for selection
|
||||
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
|
||||
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
|
||||
taColorOnAccentColor.recycle()
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
@@ -130,6 +146,7 @@ class NodeAdapter (private val context: Context,
|
||||
)
|
||||
)
|
||||
|
||||
this.mShowEntryColors = PreferencesUtil.showEntryColors(context)
|
||||
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||
@@ -145,6 +162,8 @@ class NodeAdapter (private val context: Context,
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
fun rebuildList(group: Group) {
|
||||
mOldVirtualGroup = mVirtualGroup
|
||||
mVirtualGroup = group.isVirtual
|
||||
assignPreferences()
|
||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||
}
|
||||
@@ -155,14 +174,19 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
if (mOldVirtualGroup != mVirtualGroup)
|
||||
return false
|
||||
var typeContentTheSame = true
|
||||
if (oldItem is Entry && newItem is Entry) {
|
||||
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
||||
&& oldItem.username == newItem.username
|
||||
&& oldItem.backgroundColor == newItem.backgroundColor
|
||||
&& oldItem.foregroundColor == newItem.foregroundColor
|
||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||
} else if (oldItem is Group && newItem is Group) {
|
||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||
&& oldItem.notes == newItem.notes
|
||||
}
|
||||
return typeContentTheSame
|
||||
&& oldItem.nodeId == newItem.nodeId
|
||||
@@ -323,23 +347,6 @@ class NodeAdapter (private val context: Context,
|
||||
isSelected = mActionNodesList.contains(subNode)
|
||||
}
|
||||
|
||||
// Assign image
|
||||
val iconColor = if (holder.container.isSelected)
|
||||
mContentSelectionColor
|
||||
else when (subNode.type) {
|
||||
Type.GROUP -> mIconGroupColor
|
||||
Type.ENTRY -> mIconEntryColor
|
||||
}
|
||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||
holder.icon.apply {
|
||||
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
@@ -348,14 +355,32 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
// Add meta text to show UUID
|
||||
holder.meta.apply {
|
||||
if (mShowUUID) {
|
||||
text = subNode.nodeId.toString()
|
||||
val nodeId = subNode.nodeId?.toVisualString()
|
||||
if (mShowUUID && nodeId != null) {
|
||||
text = nodeId
|
||||
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
// Add path to virtual group
|
||||
if (mVirtualGroup) {
|
||||
holder.path?.apply {
|
||||
text = subNode.getPathString()
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.path?.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Assign icon colors
|
||||
var iconColor = if (holder.container.isSelected)
|
||||
mColorOnAccentColor
|
||||
else when (subNode.type) {
|
||||
Type.GROUP -> mTextColorPrimary
|
||||
Type.ENTRY -> mTextColor
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
@@ -398,6 +423,44 @@ class NodeAdapter (private val context: Context,
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
// Assign colors
|
||||
val backgroundColor = if (mShowEntryColors) entry.backgroundColor else null
|
||||
if (!holder.container.isSelected) {
|
||||
if (backgroundColor != null) {
|
||||
holder.container.setBackgroundColor(backgroundColor)
|
||||
} else {
|
||||
holder.container.setBackgroundColor(Color.TRANSPARENT)
|
||||
}
|
||||
} else {
|
||||
holder.container.setBackgroundColor(mColorAccentLight)
|
||||
}
|
||||
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
|
||||
if (!holder.container.isSelected) {
|
||||
if (foregroundColor != null) {
|
||||
holder.text.setTextColor(foregroundColor)
|
||||
holder.subText?.setTextColor(foregroundColor)
|
||||
holder.otpToken?.setTextColor(foregroundColor)
|
||||
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||
holder.meta.setTextColor(foregroundColor)
|
||||
iconColor = foregroundColor
|
||||
} else {
|
||||
holder.text.setTextColor(mTextColor)
|
||||
holder.subText?.setTextColor(mTextColorSecondary)
|
||||
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||
holder.meta.setTextColor(mTextColor)
|
||||
}
|
||||
} else {
|
||||
holder.text.setTextColor(mColorOnAccentColor)
|
||||
holder.subText?.setTextColor(mColorOnAccentColor)
|
||||
holder.otpToken?.setTextColor(mColorOnAccentColor)
|
||||
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
|
||||
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
|
||||
holder.meta.setTextColor(mColorOnAccentColor)
|
||||
}
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
@@ -416,6 +479,17 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
// Assign image
|
||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||
holder.icon.apply {
|
||||
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// Assign click
|
||||
holder.container.setOnClickListener {
|
||||
mNodeClickCallback?.onNodeClick(database, subNode)
|
||||
@@ -430,15 +504,16 @@ class NodeAdapter (private val context: Context,
|
||||
OtpType.HOTP -> {
|
||||
holder?.otpProgress?.apply {
|
||||
max = 100
|
||||
progress = 100
|
||||
setProgressCompat(100, true)
|
||||
}
|
||||
}
|
||||
OtpType.TOTP -> {
|
||||
holder?.otpProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
setProgressCompat(otpElement.secondsRemaining, true)
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.token
|
||||
@@ -497,8 +572,9 @@ class NodeAdapter (private val context: Context,
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView? = itemView.findViewById(R.id.node_subtext)
|
||||
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||
var path: TextView? = itemView.findViewById(R.id.node_path)
|
||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
||||
var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress)
|
||||
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
@@ -506,6 +582,6 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = NodeAdapter::class.java.name
|
||||
private val TAG = NodesAdapter::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
|
||||
class SearchEntryCursorAdapter(private val context: Context,
|
||||
private val database: Database)
|
||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
private var mDisplayUsername: Boolean = false
|
||||
private var mOmitBackup: Boolean = true
|
||||
private val iconColor: Int
|
||||
|
||||
init {
|
||||
// Get the icon color
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
this.iconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
|
||||
reInit(context)
|
||||
}
|
||||
|
||||
fun reInit(context: Context) {
|
||||
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mOmitBackup = PreferencesUtil.omitBackup(context)
|
||||
}
|
||||
|
||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||
|
||||
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
||||
val viewHolder = ViewHolder()
|
||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
|
||||
view.tag = viewHolder
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||
getEntryFrom(cursor)?.let { currentEntry ->
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
|
||||
// Assign image
|
||||
viewHolder.imageViewIcon?.let { iconView ->
|
||||
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
|
||||
}
|
||||
|
||||
// Assign title
|
||||
viewHolder.textViewTitle?.apply {
|
||||
text = currentEntry.getVisualTitle()
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
|
||||
// Assign subtitle
|
||||
viewHolder.textViewSubTitle?.apply {
|
||||
val entryUsername = currentEntry.username
|
||||
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
|
||||
String.format("(%s)", entryUsername)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||
return database.createEntry()?.apply {
|
||||
entryKDB?.let { entryKDB ->
|
||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||
{ standardIconId ->
|
||||
database.getStandardIcon(standardIconId)
|
||||
},
|
||||
{ customIconId ->
|
||||
database.getCustomIcon(customIconId)
|
||||
}
|
||||
)
|
||||
}
|
||||
entryKDBX?.let { entryKDBX ->
|
||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
|
||||
{ standardIconId ->
|
||||
database.getStandardIcon(standardIconId)
|
||||
},
|
||||
{ customIconId ->
|
||||
database.getCustomIcon(customIconId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||
return searchEntries(context, constraint.toString())
|
||||
}
|
||||
|
||||
private fun searchEntries(context: Context, query: String): Cursor? {
|
||||
var cursorKDB: EntryCursorKDB? = null
|
||||
var cursorKDBX: EntryCursorKDBX? = null
|
||||
|
||||
if (database.type == DatabaseKDB.TYPE)
|
||||
cursorKDB = EntryCursorKDB()
|
||||
if (database.type == DatabaseKDBX.TYPE)
|
||||
cursorKDBX = EntryCursorKDBX()
|
||||
|
||||
val searchGroup = database.createVirtualGroupFromSearch(query,
|
||||
mOmitBackup,
|
||||
SearchHelper.MAX_SEARCH_ENTRY)
|
||||
if (searchGroup != null) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||
database.startManageEntry(entry)
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
cursorKDBX?.addEntry(it)
|
||||
}
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return cursorKDB ?: cursorKDBX
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): Entry? {
|
||||
var pwEntry: Entry? = null
|
||||
|
||||
val cursor = this.cursor
|
||||
if (cursor.moveToFirst() && cursor.move(position)) {
|
||||
pwEntry = getEntryFrom(cursor)
|
||||
}
|
||||
return pwEntry
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
var imageViewIcon: ImageView? = null
|
||||
var textViewTitle: TextView? = null
|
||||
var textViewSubTitle: TextView? = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Tags
|
||||
|
||||
class TagsAdapter(context: Context) : RecyclerView.Adapter<TagsAdapter.TagViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mTags: Tags = Tags()
|
||||
var onItemClickListener: OnItemClickListener? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagViewHolder {
|
||||
val view = inflater.inflate(R.layout.item_tag, parent, false)
|
||||
return TagViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TagViewHolder, position: Int) {
|
||||
val field = mTags.get(position)
|
||||
holder.name.text = field
|
||||
holder.bind(field, onItemClickListener)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mTags.size()
|
||||
}
|
||||
|
||||
fun setTags(tags: Tags) {
|
||||
mTags.setTags(tags)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mTags.clear()
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(item: String)
|
||||
}
|
||||
|
||||
inner class TagViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var name: TextView = itemView.findViewById(R.id.tag_name)
|
||||
|
||||
fun bind(item: String, listener: OnItemClickListener?) {
|
||||
itemView.setOnClickListener { listener?.onItemClick(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Tags
|
||||
import com.tokenautocomplete.FilteredArrayAdapter
|
||||
|
||||
class TagsProposalAdapter(context: Context, proposal: Tags?)
|
||||
: FilteredArrayAdapter<String>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_1,
|
||||
(proposal ?: Tags()).toList()
|
||||
) {
|
||||
|
||||
override fun keepObject(obj: String, mask: String?): Boolean {
|
||||
if (mask == null)
|
||||
return false
|
||||
return obj.contains(mask, true)
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,23 @@ import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
|
||||
|
||||
class TemplatesSelectorAdapter(private val context: Context,
|
||||
class TemplatesSelectorAdapter(
|
||||
context: Context,
|
||||
private var templates: List<Template>): BaseAdapter() {
|
||||
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mIconColor = Color.BLACK
|
||||
private var mTextColor = Color.BLACK
|
||||
|
||||
init {
|
||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
@@ -36,6 +36,7 @@ class TemplatesSelectorAdapter(private val context: Context,
|
||||
if (templateView == null) {
|
||||
holder = TemplateSelectorViewHolder()
|
||||
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
||||
holder.background = templateView?.findViewById(R.id.template_background)
|
||||
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||
holder.name = templateView?.findViewById(R.id.template_name)
|
||||
templateView?.tag = holder
|
||||
@@ -43,10 +44,15 @@ class TemplatesSelectorAdapter(private val context: Context,
|
||||
holder = templateView.tag as TemplateSelectorViewHolder
|
||||
}
|
||||
|
||||
holder.background?.setBackgroundColor(template.backgroundColor ?: Color.TRANSPARENT)
|
||||
val textColor = template.foregroundColor ?: mTextColor
|
||||
holder.icon?.let { icon ->
|
||||
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
|
||||
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, textColor)
|
||||
}
|
||||
holder.name?.apply {
|
||||
setTextColor(textColor)
|
||||
text = TemplateField.getLocalizedName(context, template.title)
|
||||
}
|
||||
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
|
||||
|
||||
return templateView!!
|
||||
}
|
||||
@@ -64,6 +70,7 @@ class TemplatesSelectorAdapter(private val context: Context,
|
||||
}
|
||||
|
||||
inner class TemplateSelectorViewHolder {
|
||||
var background: View? = null
|
||||
var icon: ImageView? = null
|
||||
var name: TextView? = null
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ package com.kunzisoft.keepass.app.database
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
@@ -125,15 +127,40 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
|
||||
fun getCipherDatabase(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
||||
val cipherDatabase = CipherEncryptDatabase().apply {
|
||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||
this.encryptedValue = Base64.decode(
|
||||
cipherDatabaseEntity.encryptedValue,
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
this.specParameters = Base64.decode(
|
||||
cipherDatabaseEntity.specParameters,
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
}
|
||||
cipherDatabaseResultListener.invoke(cipherDatabase)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||
this.encryptedValue = Base64.decode(
|
||||
cipherDatabaseEntity.encryptedValue,
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
this.specParameters = Base64.decode(
|
||||
cipherDatabaseEntity.specParameters,
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener.invoke(it)
|
||||
@@ -149,8 +176,16 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
cipherEncryptDatabase.databaseUri?.let { databaseUri ->
|
||||
|
||||
val cipherDatabaseEntity = CipherDatabaseEntity(
|
||||
databaseUri.toString(),
|
||||
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
|
||||
Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
|
||||
)
|
||||
|
||||
if (useTempDao) {
|
||||
// The only case to create service (not needed to get an info)
|
||||
serviceActionTask(true) {
|
||||
@@ -160,7 +195,8 @@ class CipherDatabaseAction(context: Context) {
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||
val cipherDatabaseRetrieve =
|
||||
cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||
// Update values if element not yet in the database
|
||||
if (cipherDatabaseRetrieve == null) {
|
||||
cipherDatabaseDao.add(cipherDatabaseEntity)
|
||||
@@ -174,6 +210,7 @@ class CipherDatabaseAction(context: Context) {
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
|
||||
@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
|
||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||
val inlineSuggestionsRequest: InlineSuggestionsRequest?)
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||
@@ -34,7 +34,6 @@ import android.service.autofill.InlinePresentation
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
@@ -63,7 +62,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
object AutofillHelper {
|
||||
|
||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
|
||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
@@ -112,7 +111,7 @@ object AutofillHelper {
|
||||
database: Database,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset? {
|
||||
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
||||
val title = makeEntryTitle(entryInfo)
|
||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||
val builder = Dataset.Builder(views)
|
||||
@@ -201,11 +200,7 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
additionalBuild?.invoke(builder)
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
@@ -236,14 +231,16 @@ object AutofillHelper {
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: Database,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
|
||||
if (positionItem <= maxSuggestion - 1
|
||||
&& inlinePresentationSpecs.size > positionItem) {
|
||||
&& inlinePresentationSpecs.size > positionItem
|
||||
) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
@@ -252,20 +249,23 @@ object AutofillHelper {
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(context,
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, AutofillSettingsActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
})
|
||||
}
|
||||
)
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(entryInfo.title)
|
||||
setSubtitle(entryInfo.username)
|
||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setStartIcon(
|
||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||
@@ -273,7 +273,9 @@ object AutofillHelper {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}.build().slice, inlinePresentationSpec, false
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -303,7 +305,7 @@ object AutofillHelper {
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -324,7 +326,7 @@ object AutofillHelper {
|
||||
// Add inline suggestion for new IME and dataset
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
@@ -336,14 +338,19 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
||||
if (numberInlineSuggestions > 0
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
||||
buildInlinePresentationForEntry(context, database,
|
||||
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
||||
)?.let { inlinePresentation ->
|
||||
builder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
null
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
||||
}
|
||||
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
@@ -355,14 +362,14 @@ object AutofillHelper {
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
searchInfo, compatInlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
@@ -407,11 +414,11 @@ object AutofillHelper {
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (inlineSuggestionsRequest != null) {
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (compatInlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
|
||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
||||
} else {
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
@@ -464,7 +471,7 @@ object AutofillHelper {
|
||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||
autofillComponent.inlineSuggestionsRequest?.let {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.service.autofill.FillRequest
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class CompatInlineSuggestionsRequest : Parcelable {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.R)
|
||||
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
|
||||
private set
|
||||
|
||||
constructor(fillRequest: FillRequest) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
|
||||
} else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
|
||||
this.inlineSuggestionsRequest = inlineSuggestionsRequest
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest =
|
||||
parcel.readParcelable(FillRequest::class.java.classLoader)
|
||||
}
|
||||
else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
parcel.writeParcelable(inlineSuggestionsRequest, flags)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
|
||||
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
|
||||
return CompatInlineSuggestionsRequest(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
request.inlineSuggestionsRequest
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
|
||||
private fun launchSelection(database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
inlineSuggestionsRequest?.let {
|
||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||
&& inlinePresentationSpecs.size > 0) {
|
||||
@@ -281,8 +281,9 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
// Build response
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
}
|
||||
} else {
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
}
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
@@ -60,6 +63,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
var databaseFileUri: Uri? = null
|
||||
private set
|
||||
|
||||
// TODO Retrieve credential storage from app database
|
||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||
|
||||
/**
|
||||
* Manage setting to auto open biometric prompt
|
||||
*/
|
||||
@@ -477,6 +483,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
} ?: 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")
|
||||
@@ -528,16 +535,29 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
|
||||
mBuilderListener?.onCredentialEncrypted(
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.encryptedValue = encryptedValue
|
||||
this.specParameters = ivSpec
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: String) {
|
||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
||||
// Load database directly with password retrieve
|
||||
databaseFileUri?.let {
|
||||
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
mBuilderListener?.onCredentialDecrypted(
|
||||
CipherDecryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.decryptedValue = decryptedValue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +571,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
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)
|
||||
@@ -580,6 +601,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.message = text
|
||||
@@ -617,10 +639,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
|
||||
interface BuilderListener {
|
||||
fun retrieveCredentialForEncryption(): String
|
||||
fun retrieveCredentialForEncryption(): ByteArray
|
||||
fun conditionToStoreCredential(): Boolean
|
||||
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
|
||||
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
|
||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
|
||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
||||
@@ -27,7 +27,6 @@ import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
@@ -214,18 +213,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptData(value: String) {
|
||||
fun encryptData(value: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val encrypted = cipher?.doFinal(value.toByteArray())
|
||||
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||
|
||||
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 ->
|
||||
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
|
||||
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
|
||||
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to encrypt data", e)
|
||||
@@ -233,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun initDecryptData(ivSpecValue: String,
|
||||
fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initDecryptData(ivSpecValue: String,
|
||||
private fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean = true) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
@@ -246,9 +242,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
try {
|
||||
// important to restore spec here that was used for decryption
|
||||
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
|
||||
val spec = IvParameterSpec(iv)
|
||||
|
||||
val spec = IvParameterSpec(ivSpecValue)
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
@@ -284,15 +278,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptData(encryptedValue: String) {
|
||||
fun decryptData(encryptedValue: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// actual decryption here
|
||||
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
||||
cipher?.doFinal(encrypted)?.let { decrypted ->
|
||||
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
|
||||
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||
advancedUnlockCallback?.handleDecryptedResult(decrypted)
|
||||
}
|
||||
} catch (badPaddingException: BadPaddingException) {
|
||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
||||
@@ -367,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
fun onAuthenticationSucceeded()
|
||||
fun onAuthenticationFailed()
|
||||
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
|
||||
fun handleDecryptedResult(decryptedValue: String)
|
||||
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
|
||||
fun handleDecryptedResult(decryptedValue: ByteArray)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -469,9 +462,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
advancedCallback.onUnrecoverableKeyException(e)
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
open class AssignPasswordInDatabaseRunnable (
|
||||
open class AssignMainCredentialInDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
|
||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
} catch (e: Exception) {
|
||||
erase(mBackupKey)
|
||||
setError(e)
|
||||
@@ -35,7 +35,7 @@ class CreateDatabaseRunnable(context: Context,
|
||||
private val templateGroupName: String?,
|
||||
mainCredential: MainCredential,
|
||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
|
||||
@@ -27,12 +27,12 @@ import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
@@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||
@@ -82,7 +84,6 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||
@@ -342,18 +343,27 @@ class DatabaseTaskProvider {
|
||||
fun startDatabaseLoad(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEntity: CipherDatabaseEntity?,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
, ACTION_DATABASE_MERGE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
@@ -361,6 +371,19 @@ class DatabaseTaskProvider {
|
||||
, ACTION_DATABASE_RELOAD_TASK)
|
||||
}
|
||||
|
||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||
if (conditionToAsk) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.warning_database_info_reloaded)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
approved.invoke()
|
||||
}.create().show()
|
||||
} else {
|
||||
approved.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
|
||||
@@ -671,9 +694,10 @@ class DatabaseTaskProvider {
|
||||
/**
|
||||
* Save Database without parameter
|
||||
*/
|
||||
fun startDatabaseSave(save: Boolean) {
|
||||
fun startDatabaseSave(save: Boolean, saveToUri: Uri? = null) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -39,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mUri: Uri,
|
||||
private val mMainCredential: MainCredential,
|
||||
private val mReadonly: Boolean,
|
||||
private val mCipherEntity: CipherDatabaseEntity?,
|
||||
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
@@ -60,7 +59,6 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
LoadedKey.generateNewCipherKey(),
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
@@ -77,9 +75,9 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
}
|
||||
|
||||
// Register the biometric
|
||||
mCipherEntity?.let { cipherDatabaseEntity ->
|
||||
mCipherEncryptDatabase?.let { cipherDatabase ->
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
||||
.addOrUpdateCipherDatabase(cipherDatabase) // return value not called
|
||||
}
|
||||
|
||||
// Register the current time to init the lock timer
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
|
||||
class MergeDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onStartRun() {
|
||||
mDatabase.wasReloaded = true
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.mergeData(mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
context.contentResolver,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater
|
||||
)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
mLoadDatabaseResult?.invoke(result)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
|
||||
private var tempCipherKey: LoadedKey? = null
|
||||
|
||||
override fun onStartRun() {
|
||||
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
|
||||
// Clear before we load
|
||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
||||
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
|
||||
mDatabase.wasReloaded = true
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.reloadData(context.contentResolver,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
|
||||
progressTaskUpdater)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
} else {
|
||||
tempCipherKey = null
|
||||
mDatabase.clearAndClose(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private var saveDatabase: Boolean)
|
||||
private var saveDatabase: Boolean,
|
||||
private var databaseCopyUri: Uri? = null)
|
||||
: ActionRunnable() {
|
||||
|
||||
var mAfterSaveDatabase: ((Result) -> Unit)? = null
|
||||
@@ -34,9 +36,10 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
override fun onStartRun() {}
|
||||
|
||||
override fun onActionRun() {
|
||||
database.checkVersion()
|
||||
if (saveDatabase && result.isSuccess) {
|
||||
try {
|
||||
database.saveData(context.contentResolver)
|
||||
database.saveData(databaseCopyUri, context.contentResolver)
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
|
||||
abstract fun nodeAction()
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
nodeAction()
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
|
||||
|
||||
foreachNode@ for(nodeToDelete in mNodesToDelete) {
|
||||
mOldParent = nodeToDelete.parent
|
||||
mOldParent?.touch(modified = false, touchParents = true)
|
||||
nodeToDelete.touch(modified = true, touchParents = true)
|
||||
|
||||
when (nodeToDelete.type) {
|
||||
Type.GROUP -> {
|
||||
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
|
||||
// Remove Node from parent
|
||||
mCanRecycle = database.canRecycle(groupToDelete)
|
||||
if (mCanRecycle) {
|
||||
groupToDelete.touch(modified = false, touchParents = true)
|
||||
database.recycle(groupToDelete, context.resources)
|
||||
groupToDelete.setPreviousParentGroup(mOldParent)
|
||||
groupToDelete.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
database.deleteGroup(groupToDelete)
|
||||
}
|
||||
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
|
||||
// Remove Node from parent
|
||||
mCanRecycle = database.canRecycle(entryToDelete)
|
||||
if (mCanRecycle) {
|
||||
entryToDelete.touch(modified = false, touchParents = true)
|
||||
database.recycle(entryToDelete, context.resources)
|
||||
entryToDelete.setPreviousParentGroup(mOldParent)
|
||||
entryToDelete.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
database.deleteEntry(entryToDelete)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
|
||||
foreachNode@ for(nodeToMove in mNodesToMove) {
|
||||
// Move node in new parent
|
||||
mOldParent = nodeToMove.parent
|
||||
nodeToMove.touch(modified = true, touchParents = true)
|
||||
|
||||
when (nodeToMove.type) {
|
||||
Type.GROUP -> {
|
||||
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
|
||||
// and if not in the current group
|
||||
&& groupToMove != mNewParent
|
||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||
groupToMove.touch(modified = true, touchParents = true)
|
||||
database.moveGroupTo(groupToMove, mNewParent)
|
||||
groupToMove.setPreviousParentGroup(mOldParent)
|
||||
groupToMove.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(MoveGroupDatabaseException())
|
||||
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
|
||||
if (mOldParent != mNewParent
|
||||
// and root can contains entry
|
||||
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
||||
entryToMove.touch(modified = true, touchParents = true)
|
||||
database.moveEntryTo(entryToMove, mNewParent)
|
||||
entryToMove.setPreviousParentGroup(mOldParent)
|
||||
entryToMove.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(MoveEntryDatabaseException())
|
||||
|
||||
@@ -42,6 +42,9 @@ class UpdateGroupRunnable constructor(
|
||||
// Update group with new values
|
||||
mNewGroup.touch(modified = true, touchParents = true)
|
||||
|
||||
if (database.rootGroup == mOldGroup) {
|
||||
database.rootGroup = mNewGroup
|
||||
}
|
||||
// Only change data in index
|
||||
database.updateGroup(mNewGroup)
|
||||
}
|
||||
@@ -50,6 +53,9 @@ class UpdateGroupRunnable constructor(
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
if (database.rootGroup == mNewGroup) {
|
||||
database.rootGroup = mOldGroup
|
||||
}
|
||||
database.updateGroup(mOldGroup)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.provider.BaseColumns
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import java.util.*
|
||||
|
||||
abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>> : MatrixCursor(arrayOf(
|
||||
_ID,
|
||||
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
|
||||
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS,
|
||||
COLUMN_INDEX_TITLE,
|
||||
COLUMN_INDEX_ICON_STANDARD,
|
||||
COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS,
|
||||
COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS,
|
||||
COLUMN_INDEX_USERNAME,
|
||||
COLUMN_INDEX_PASSWORD,
|
||||
COLUMN_INDEX_URL,
|
||||
COLUMN_INDEX_NOTES,
|
||||
COLUMN_INDEX_EXPIRY_TIME,
|
||||
COLUMN_INDEX_EXPIRES
|
||||
)) {
|
||||
|
||||
protected var entryId: Long = 0
|
||||
|
||||
abstract fun addEntry(entry: PwEntryV)
|
||||
|
||||
abstract fun getPwNodeId(): NodeId<EntryId>
|
||||
|
||||
open fun populateEntry(pwEntry: PwEntryV,
|
||||
retrieveStandardIcon: (Int) -> IconImageStandard,
|
||||
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
||||
pwEntry.nodeId = getPwNodeId()
|
||||
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
||||
|
||||
val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
||||
val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
pwEntry.icon = IconImage(iconStandard, iconCustom)
|
||||
|
||||
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
|
||||
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
||||
pwEntry.url = getString(getColumnIndex(COLUMN_INDEX_URL))
|
||||
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
|
||||
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
|
||||
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
|
||||
.lowercase(Locale.ENGLISH) != "false"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val _ID = BaseColumns._ID
|
||||
const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits"
|
||||
const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits"
|
||||
const val COLUMN_INDEX_TITLE = "title"
|
||||
const val COLUMN_INDEX_ICON_STANDARD = "icon_standard"
|
||||
const val COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS = "icon_custom_UUID_most_significant_bits"
|
||||
const val COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS = "icon_custom_UUID_least_significant_bits"
|
||||
const val COLUMN_INDEX_USERNAME = "username"
|
||||
const val COLUMN_INDEX_PASSWORD = "password"
|
||||
const val COLUMN_INDEX_URL = "URL"
|
||||
const val COLUMN_INDEX_NOTES = "notes"
|
||||
const val COLUMN_INDEX_EXPIRY_TIME = "expiry_time"
|
||||
const val COLUMN_INDEX_EXPIRES = "expires"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
|
||||
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
||||
|
||||
override fun addEntry(entry: EntryKDB) {
|
||||
addRow(arrayOf(
|
||||
entryId,
|
||||
entry.id.mostSignificantBits,
|
||||
entry.id.leastSignificantBits,
|
||||
entry.title,
|
||||
entry.icon.standard.id,
|
||||
entry.icon.custom.uuid.mostSignificantBits,
|
||||
entry.icon.custom.uuid.leastSignificantBits,
|
||||
entry.username,
|
||||
entry.password,
|
||||
entry.url,
|
||||
entry.notes,
|
||||
entry.expiryTime,
|
||||
entry.expires
|
||||
))
|
||||
entryId++
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import java.util.*
|
||||
|
||||
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
||||
|
||||
private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor()
|
||||
|
||||
override fun addEntry(entry: EntryKDBX) {
|
||||
addRow(arrayOf(
|
||||
entryId,
|
||||
entry.id.mostSignificantBits,
|
||||
entry.id.leastSignificantBits,
|
||||
entry.title,
|
||||
entry.icon.standard.id,
|
||||
entry.icon.custom.uuid.mostSignificantBits,
|
||||
entry.icon.custom.uuid.leastSignificantBits,
|
||||
entry.username,
|
||||
entry.password,
|
||||
entry.url,
|
||||
entry.notes,
|
||||
entry.expiryTime,
|
||||
entry.expires
|
||||
))
|
||||
|
||||
entry.doForEachDecodedCustomField { field ->
|
||||
extraFieldCursor.addExtraField(entryId, field)
|
||||
}
|
||||
|
||||
entryId++
|
||||
}
|
||||
|
||||
override fun populateEntry(pwEntry: EntryKDBX,
|
||||
retrieveStandardIcon: (Int) -> IconImageStandard,
|
||||
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
||||
super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
|
||||
|
||||
// Retrieve extra fields
|
||||
if (extraFieldCursor.moveToFirst()) {
|
||||
while (!extraFieldCursor.isAfterLast) {
|
||||
// Add a new extra field only if entryId is the one we want
|
||||
if (extraFieldCursor.getLong(extraFieldCursor
|
||||
.getColumnIndex(ExtraFieldCursor.FOREIGN_KEY_ENTRY_ID))
|
||||
== getLong(getColumnIndex(_ID))) {
|
||||
extraFieldCursor.populateExtraFieldInEntry(pwEntry)
|
||||
}
|
||||
extraFieldCursor.moveToNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import java.util.*
|
||||
|
||||
abstract class EntryCursorUUID<EntryV: EntryVersioned<*, UUID, *, *>>: EntryCursor<UUID, EntryV>() {
|
||||
|
||||
override fun getPwNodeId(): NodeId<UUID> {
|
||||
return NodeIdUUID(
|
||||
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.provider.BaseColumns
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
|
||||
class ExtraFieldCursor : MatrixCursor(arrayOf(
|
||||
_ID,
|
||||
FOREIGN_KEY_ENTRY_ID,
|
||||
COLUMN_LABEL,
|
||||
COLUMN_PROTECTION,
|
||||
COLUMN_VALUE
|
||||
)) {
|
||||
|
||||
private var fieldId: Long = 0
|
||||
|
||||
@Synchronized
|
||||
fun addExtraField(entryId: Long, field: Field) {
|
||||
addRow(arrayOf(fieldId,
|
||||
entryId,
|
||||
field.name,
|
||||
if (field.protectedValue.isProtected) 1 else 0,
|
||||
field.protectedValue.toString()))
|
||||
fieldId++
|
||||
}
|
||||
|
||||
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
|
||||
pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)),
|
||||
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
|
||||
getString(getColumnIndex(COLUMN_VALUE))))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val _ID = BaseColumns._ID
|
||||
const val FOREIGN_KEY_ENTRY_ID = "entry_id"
|
||||
const val COLUMN_LABEL = "label"
|
||||
const val COLUMN_PROTECTION = "protection"
|
||||
const val COLUMN_VALUE = "value"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
@@ -17,7 +36,10 @@ class CustomData : Parcelable {
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java)
|
||||
mCustomDataItems.clear()
|
||||
mCustomDataItems.putAll(ParcelableUtil
|
||||
.readStringParcelableMap(parcel, CustomDataItem::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
fun get(key: String): CustomDataItem? {
|
||||
@@ -46,6 +68,10 @@ class CustomData : Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return mCustomDataItems.toString()
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ class CustomDataItem : Parcelable {
|
||||
this.lastModificationTime = lastModificationTime
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(key)
|
||||
parcel.writeString(value)
|
||||
|
||||
@@ -22,17 +22,16 @@ package com.kunzisoft.keepass.database.element
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
@@ -62,7 +62,6 @@ import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class Database {
|
||||
@@ -94,6 +93,8 @@ class Database {
|
||||
*/
|
||||
var wasReloaded = false
|
||||
|
||||
var dataModifiedSinceLastLoading = false
|
||||
|
||||
var loadTimestamp: Long? = null
|
||||
private set
|
||||
|
||||
@@ -112,7 +113,7 @@ class Database {
|
||||
|
||||
private val iconsManager: IconsManager
|
||||
get() {
|
||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
|
||||
}
|
||||
|
||||
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
||||
@@ -130,7 +131,7 @@ class Database {
|
||||
return iconsManager.doForEachCustomIcon(action)
|
||||
}
|
||||
|
||||
fun getCustomIcon(iconId: UUID): IconImageCustom {
|
||||
fun getCustomIcon(iconId: UUID): IconImageCustom? {
|
||||
return iconsManager.getIcon(iconId)
|
||||
}
|
||||
|
||||
@@ -144,11 +145,12 @@ class Database {
|
||||
|
||||
fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||
iconDrawableFactory.clearFromCache(customIcon)
|
||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||
iconsManager.removeCustomIcon(customIcon.uuid, binaryCache)
|
||||
mDatabaseKDBX?.addDeletedObject(customIcon.uuid)
|
||||
}
|
||||
|
||||
fun updateCustomIcon(customIcon: IconImageCustom) {
|
||||
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
|
||||
iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon)
|
||||
}
|
||||
|
||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||
@@ -212,6 +214,7 @@ class Database {
|
||||
set(name) {
|
||||
mDatabaseKDBX?.name = name
|
||||
mDatabaseKDBX?.nameChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowDescription: Boolean
|
||||
@@ -224,33 +227,39 @@ class Database {
|
||||
set(description) {
|
||||
mDatabaseKDBX?.description = description
|
||||
mDatabaseKDBX?.descriptionChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowDefaultUsername: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
|
||||
|
||||
var defaultUsername: String
|
||||
get() {
|
||||
return mDatabaseKDBX?.defaultUserName ?: "" // TODO mDatabaseKDB default username
|
||||
return mDatabaseKDB?.defaultUserName ?: mDatabaseKDBX?.defaultUserName ?: ""
|
||||
}
|
||||
set(username) {
|
||||
mDatabaseKDB?.defaultUserName = username
|
||||
mDatabaseKDBX?.defaultUserName = username
|
||||
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowCustomColor: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
|
||||
|
||||
// with format "#000000"
|
||||
var customColor: String
|
||||
var customColor: Int?
|
||||
get() {
|
||||
return mDatabaseKDBX?.color ?: "" // TODO mDatabaseKDB color
|
||||
var colorInt: Int? = null
|
||||
mDatabaseKDBX?.color?.let {
|
||||
try {
|
||||
colorInt = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return mDatabaseKDB?.color ?: colorInt
|
||||
}
|
||||
set(value) {
|
||||
// TODO Check color string
|
||||
mDatabaseKDBX?.color = value
|
||||
mDatabaseKDB?.color = value
|
||||
mDatabaseKDBX?.color = if (value == null) {
|
||||
""
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowOTP: Boolean
|
||||
@@ -259,6 +268,15 @@ class Database {
|
||||
val version: String
|
||||
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
|
||||
|
||||
fun checkVersion() {
|
||||
mDatabaseKDBX?.getMinKdbxVersion()?.let {
|
||||
mDatabaseKDBX?.kdbxVersion = it
|
||||
}
|
||||
}
|
||||
|
||||
val defaultFileExtension: String
|
||||
get() = mDatabaseKDB?.defaultFileExtension ?: mDatabaseKDBX?.defaultFileExtension ?: ".bin"
|
||||
|
||||
val type: Class<*>?
|
||||
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
|
||||
|
||||
@@ -274,6 +292,8 @@ class Database {
|
||||
value?.let {
|
||||
mDatabaseKDBX?.compressionAlgorithm = it
|
||||
}
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
fun compressionForNewEntry(): Boolean {
|
||||
@@ -290,6 +310,7 @@ class Database {
|
||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowNoMasterKey: Boolean
|
||||
@@ -309,8 +330,6 @@ class Database {
|
||||
set(algorithm) {
|
||||
algorithm?.let {
|
||||
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
||||
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
||||
mDatabaseKDBX?.cipherUuid = algorithm.uuid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,13 +342,10 @@ class Database {
|
||||
var kdfEngine: KdfEngine?
|
||||
get() = mDatabaseKDB?.kdfEngine ?: mDatabaseKDBX?.kdfEngine
|
||||
set(kdfEngine) {
|
||||
kdfEngine?.let {
|
||||
if (mDatabaseKDBX?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
|
||||
mDatabaseKDBX?.kdfParameters = kdfEngine.defaultParameters
|
||||
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
|
||||
memoryUsage = kdfEngine.defaultMemoryUsage
|
||||
parallelism = kdfEngine.defaultParallelism
|
||||
}
|
||||
mDatabaseKDB?.kdfEngine = kdfEngine
|
||||
mDatabaseKDBX?.kdfEngine = kdfEngine
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
fun getKeyDerivationName(): String {
|
||||
@@ -341,6 +357,8 @@ class Database {
|
||||
set(numberRounds) {
|
||||
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
|
||||
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var memoryUsage: Long
|
||||
@@ -349,12 +367,16 @@ class Database {
|
||||
}
|
||||
set(memory) {
|
||||
mDatabaseKDBX?.memoryUsage = memory
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var parallelism: Long
|
||||
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
||||
set(parallelism) {
|
||||
mDatabaseKDBX?.parallelism = parallelism
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var masterKey: ByteArray
|
||||
@@ -362,9 +384,11 @@ class Database {
|
||||
set(masterKey) {
|
||||
mDatabaseKDB?.masterKey = masterKey
|
||||
mDatabaseKDBX?.masterKey = masterKey
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val rootGroup: Group?
|
||||
var rootGroup: Group?
|
||||
get() {
|
||||
mDatabaseKDB?.rootGroup?.let {
|
||||
return Group(it)
|
||||
@@ -374,6 +398,25 @@ class Database {
|
||||
}
|
||||
return null
|
||||
}
|
||||
set(value) {
|
||||
value?.groupKDB?.let { rootKDB ->
|
||||
mDatabaseKDB?.rootGroup = rootKDB
|
||||
}
|
||||
value?.groupKDBX?.let { rootKDBX ->
|
||||
mDatabaseKDBX?.rootGroup = rootKDBX
|
||||
}
|
||||
}
|
||||
|
||||
val rootGroupIsVirtual: Boolean
|
||||
get() {
|
||||
mDatabaseKDB?.let {
|
||||
return true
|
||||
}
|
||||
mDatabaseKDBX?.let {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not modify groups here, used for read only
|
||||
@@ -393,6 +436,8 @@ class Database {
|
||||
}
|
||||
set(value) {
|
||||
mDatabaseKDBX?.historyMaxItems = value
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var historyMaxSize: Long
|
||||
@@ -401,6 +446,8 @@ class Database {
|
||||
}
|
||||
set(value) {
|
||||
mDatabaseKDBX?.historyMaxSize = value
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,15 +468,17 @@ class Database {
|
||||
} else {
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
mDatabaseKDBX?.recycleBinChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val recycleBin: Group?
|
||||
get() {
|
||||
mDatabaseKDB?.backupGroup?.let {
|
||||
return Group(it)
|
||||
return getGroupById(it.nodeId) ?: Group(it)
|
||||
}
|
||||
mDatabaseKDBX?.recycleBin?.let {
|
||||
return Group(it)
|
||||
return getGroupById(it.nodeId) ?: Group(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -439,15 +488,17 @@ class Database {
|
||||
if (group != null) {
|
||||
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
|
||||
} else {
|
||||
mDatabaseKDBX?.removeTemplatesGroup()
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
mDatabaseKDBX?.recycleBinChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a configurable templates group is available or not for this version of database
|
||||
* @return true if a configurable templates group available
|
||||
*/
|
||||
val allowConfigurableTemplatesGroup: Boolean
|
||||
val allowTemplatesGroup: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
// Maybe another templates method with KDBX5
|
||||
@@ -456,6 +507,8 @@ class Database {
|
||||
|
||||
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
|
||||
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
|
||||
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val templatesGroup: Group?
|
||||
@@ -471,8 +524,10 @@ class Database {
|
||||
if (group != null) {
|
||||
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
|
||||
} else {
|
||||
mDatabaseKDBX?.entryTemplatesGroup
|
||||
mDatabaseKDBX?.removeTemplatesGroup()
|
||||
}
|
||||
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val groupNamesNotAllowed: List<String>
|
||||
@@ -499,6 +554,7 @@ class Database {
|
||||
this.fileUri = databaseUri
|
||||
// Set Database state
|
||||
this.loaded = true
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@@ -555,7 +611,6 @@ class Database {
|
||||
contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
tempCipherKey: LoadedKey,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
@@ -576,99 +631,231 @@ class Database {
|
||||
// Read database stream for the first time
|
||||
readDatabaseStream(contentResolver, uri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||
val databaseKDB = DatabaseKDB().apply {
|
||||
binaryCache.cacheDirectory = cacheDirectory
|
||||
changeDuplicateId = fixDuplicateUUID
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
progressTaskUpdater
|
||||
) {
|
||||
databaseKDB.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater,
|
||||
fixDuplicateUUID)
|
||||
keyFileInputStream
|
||||
)
|
||||
}
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
val databaseKDBX = DatabaseKDBX().apply {
|
||||
binaryCache.cacheDirectory = cacheDirectory
|
||||
changeDuplicateId = fixDuplicateUUID
|
||||
}
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
progressTaskUpdater) {
|
||||
databaseKDBX.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater,
|
||||
fixDuplicateUUID)
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.e(TAG, "Unable to load keyfile", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun isMergeDataAllowed(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun mergeData(databaseToMergeUri: Uri?,
|
||||
databaseToMergeMainCredential: MainCredential?,
|
||||
contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
mDatabaseKDB?.let {
|
||||
throw IODatabaseException("Unable to merge from a database V1")
|
||||
}
|
||||
|
||||
// New database instance to get new changes
|
||||
val databaseToMerge = Database()
|
||||
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
try {
|
||||
val databaseUri = databaseToMerge.fileUri
|
||||
if (databaseUri != null) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
// Get keyFile inputStream
|
||||
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||
}
|
||||
}
|
||||
|
||||
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
val databaseToMergeKDB = DatabaseKDB()
|
||||
DatabaseInputKDB(databaseToMergeKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDB.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDB.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
databaseToMergeKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseToMergeKDBX = DatabaseKDBX()
|
||||
DatabaseInputKDBX(databaseToMergeKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDBX.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDBX.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseToMergeKDBX
|
||||
}
|
||||
)
|
||||
|
||||
mDatabaseKDBX?.let { currentDatabaseKDBX ->
|
||||
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
|
||||
this.isRAMSufficient = isRAMSufficient
|
||||
}
|
||||
databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge ->
|
||||
databaseMerger.merge(databaseKDBToMerge)
|
||||
if (databaseToMergeUri != null) {
|
||||
this.dataModifiedSinceLastLoading = true
|
||||
}
|
||||
}
|
||||
databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge ->
|
||||
databaseMerger.merge(databaseKDBXToMerge)
|
||||
if (databaseToMergeUri != null) {
|
||||
this.dataModifiedSinceLastLoading = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw IODatabaseException("Database URI is null, database cannot be merged")
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
databaseToMerge.clearAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun reloadData(contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
tempCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
// Retrieve the stream from the old database URI
|
||||
try {
|
||||
fileUri?.let { oldDatabaseUri ->
|
||||
val oldDatabaseUri = fileUri
|
||||
if (oldDatabaseUri != null) {
|
||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater)
|
||||
val databaseKDB = DatabaseKDB()
|
||||
mDatabaseKDB?.let {
|
||||
databaseKDB.binaryCache = it.binaryCache
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDB.masterKey = masterKey
|
||||
}
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater)
|
||||
val databaseKDBX = DatabaseKDBX()
|
||||
mDatabaseKDBX?.let {
|
||||
databaseKDBX.binaryCache = it.binaryCache
|
||||
}
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDBX.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
)
|
||||
} ?: run {
|
||||
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
||||
throw IODatabaseException()
|
||||
} else {
|
||||
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.e(TAG, "Unable to load keyfile", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun isGroupSearchable(group: Group, omitBackup: Boolean): Boolean {
|
||||
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, omitBackup) ?:
|
||||
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, omitBackup) ?:
|
||||
false
|
||||
fun groupIsInRecycleBin(group: Group): Boolean {
|
||||
val groupKDB = group.groupKDB
|
||||
val groupKDBX = group.groupKDBX
|
||||
if (groupKDB != null) {
|
||||
return mDatabaseKDB?.isInRecycleBin(groupKDB) ?: false
|
||||
} else if (groupKDBX != null) {
|
||||
return mDatabaseKDBX?.isInRecycleBin(groupKDBX) ?: false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearch(searchQuery: String,
|
||||
omitBackup: Boolean,
|
||||
fun groupIsInTemplates(group: Group): Boolean {
|
||||
val groupKDBX = group.groupKDBX
|
||||
if (groupKDBX != null) {
|
||||
return mDatabaseKDBX?.getTemplatesGroup() == groupKDBX
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
|
||||
fromGroup: NodeId<*>? = null,
|
||||
max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||
SearchParameters().apply {
|
||||
this.searchQuery = searchQuery
|
||||
}, omitBackup, max)
|
||||
searchParameters, fromGroup, max)
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||
omitBackup: Boolean,
|
||||
max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||
SearchParameters().apply {
|
||||
searchQuery = searchInfoString
|
||||
searchInTitles = true
|
||||
searchInUserNames = false
|
||||
searchInUsernames = false
|
||||
searchInPasswords = false
|
||||
searchInUrls = true
|
||||
searchInNotes = true
|
||||
@@ -676,13 +863,21 @@ class Database {
|
||||
searchInOther = true
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
searchInCurrentGroup = false
|
||||
searchInSearchableGroup = true
|
||||
searchInRecycleBin = false
|
||||
searchInTemplates = false
|
||||
}, omitBackup, max)
|
||||
}, null, max)
|
||||
}
|
||||
|
||||
val tagPool: Tags
|
||||
get() {
|
||||
return mDatabaseKDBX?.tagPool ?: Tags()
|
||||
}
|
||||
|
||||
val attachmentPool: AttachmentPool
|
||||
get() {
|
||||
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
|
||||
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool()
|
||||
}
|
||||
|
||||
val allowMultipleAttachments: Boolean
|
||||
@@ -695,8 +890,8 @@ class Database {
|
||||
}
|
||||
|
||||
fun buildNewBinaryAttachment(): BinaryData? {
|
||||
return mDatabaseKDB?.buildNewAttachment()
|
||||
?: mDatabaseKDBX?.buildNewAttachment( false,
|
||||
return mDatabaseKDB?.buildNewBinaryAttachment()
|
||||
?: mDatabaseKDBX?.buildNewBinaryAttachment( false,
|
||||
compressionForNewEntry(),
|
||||
false)
|
||||
}
|
||||
@@ -710,25 +905,16 @@ class Database {
|
||||
fun removeUnlinkedAttachments() {
|
||||
// No check in database KDB because unique attachment by entry
|
||||
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun saveData(contentResolver: ContentResolver) {
|
||||
fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) {
|
||||
try {
|
||||
this.fileUri?.let {
|
||||
saveData(contentResolver, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to save database", e)
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, DatabaseOutputException::class)
|
||||
private fun saveData(contentResolver: ContentResolver, uri: Uri) {
|
||||
|
||||
if (uri.scheme == "file") {
|
||||
uri.path?.let { filename ->
|
||||
val saveUri = databaseCopyUri ?: this.fileUri
|
||||
if (saveUri != null) {
|
||||
if (saveUri.scheme == "file") {
|
||||
saveUri.path?.let { filename ->
|
||||
val tempFile = File("$filename.tmp")
|
||||
|
||||
var fileOutputStream: FileOutputStream? = null
|
||||
@@ -757,10 +943,16 @@ class Database {
|
||||
} else {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
outputStream = contentResolver.openOutputStream(saveUri, "rwt")
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||
val databaseOutput =
|
||||
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let {
|
||||
DatabaseOutputKDBX(
|
||||
it,
|
||||
definedOutputStream
|
||||
)
|
||||
}
|
||||
databaseOutput?.output()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -769,17 +961,32 @@ class Database {
|
||||
outputStream?.close()
|
||||
}
|
||||
}
|
||||
this.fileUri = uri
|
||||
if (databaseCopyUri == null) {
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to save database", e)
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(filesDirectory: File? = null) {
|
||||
binaryCache.clear()
|
||||
iconsManager.clearCache()
|
||||
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
|
||||
this.mDatabaseKDB?.clearIndexes()
|
||||
this.mDatabaseKDBX?.clearIndexes()
|
||||
|
||||
this.mDatabaseKDB?.clearIconsCache()
|
||||
this.mDatabaseKDBX?.clearIconsCache()
|
||||
|
||||
this.mDatabaseKDB?.clearAttachmentsCache()
|
||||
this.mDatabaseKDBX?.clearAttachmentsCache()
|
||||
|
||||
this.mDatabaseKDB?.clearBinaries()
|
||||
this.mDatabaseKDBX?.clearBinaries()
|
||||
|
||||
iconDrawableFactory.clearCache()
|
||||
// Delete the cache of the database if present
|
||||
mDatabaseKDB?.clearCache()
|
||||
mDatabaseKDBX?.clearCache()
|
||||
// In all cases, delete all the files in the temp dir
|
||||
|
||||
// delete all the files in the temp dir if allowed
|
||||
try {
|
||||
filesDirectory?.let { directory ->
|
||||
cleanDirectory(directory)
|
||||
@@ -790,7 +997,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun clearAndClose(context: Context? = null) {
|
||||
clear(context?.let { UriUtil.getBinaryDir(context) })
|
||||
clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) })
|
||||
this.mDatabaseKDB = null
|
||||
this.mDatabaseKDBX = null
|
||||
this.fileUri = null
|
||||
@@ -817,9 +1024,10 @@ class Database {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||
}
|
||||
|
||||
fun rootCanContainsEntry(): Boolean {
|
||||
@@ -827,6 +1035,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun createEntry(): Entry? {
|
||||
dataModifiedSinceLastLoading = true
|
||||
mDatabaseKDB?.let { database ->
|
||||
return Entry(database.createEntry()).apply {
|
||||
nodeId = database.newEntryId()
|
||||
@@ -841,19 +1050,25 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
fun createGroup(): Group? {
|
||||
fun createGroup(virtual: Boolean = false): Group? {
|
||||
if (!virtual) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
var group: Group? = null
|
||||
mDatabaseKDB?.let { database ->
|
||||
return Group(database.createGroup()).apply {
|
||||
group = Group(database.createGroup()).apply {
|
||||
setNodeId(database.newGroupId())
|
||||
}
|
||||
}
|
||||
mDatabaseKDBX?.let { database ->
|
||||
return Group(database.createGroup()).apply {
|
||||
group = Group(database.createGroup()).apply {
|
||||
setNodeId(database.newGroupId())
|
||||
}
|
||||
}
|
||||
if (virtual)
|
||||
group?.isVirtual = virtual
|
||||
|
||||
return null
|
||||
return group
|
||||
}
|
||||
|
||||
fun getEntryById(id: NodeId<UUID>): Entry? {
|
||||
@@ -879,6 +1094,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun addEntryTo(entry: Entry, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -889,6 +1105,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun updateEntry(entry: Entry) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.updateEntry(entryKDB)
|
||||
}
|
||||
@@ -898,6 +1115,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun removeEntryFrom(entry: Entry, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -908,6 +1126,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun addGroupTo(group: Group, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.groupKDB?.let { groupKDB ->
|
||||
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -918,6 +1137,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun updateGroup(group: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.groupKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.updateGroup(entryKDB)
|
||||
}
|
||||
@@ -927,6 +1147,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun removeGroupFrom(group: Group, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.groupKDB?.let { groupKDB ->
|
||||
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -965,12 +1186,17 @@ class Database {
|
||||
}
|
||||
|
||||
fun deleteEntry(entry: Entry) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDBX?.id?.let { entryId ->
|
||||
mDatabaseKDBX?.addDeletedObject(entryId)
|
||||
}
|
||||
entry.parent?.let {
|
||||
removeEntryFrom(entry, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteGroup(group: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
@@ -980,6 +1206,9 @@ class Database {
|
||||
},
|
||||
object : NodeHandler<Group>() {
|
||||
override fun operate(node: Group): Boolean {
|
||||
node.groupKDBX?.id?.let { groupId ->
|
||||
mDatabaseKDBX?.addDeletedObject(groupId)
|
||||
}
|
||||
node.parent?.let {
|
||||
removeGroupFrom(node, it)
|
||||
}
|
||||
@@ -988,24 +1217,6 @@ class Database {
|
||||
})
|
||||
}
|
||||
|
||||
fun undoDeleteEntry(entry: Entry, parent: Group) {
|
||||
entry.entryKDB?.let {
|
||||
mDatabaseKDB?.undoDeleteEntryFrom(it, parent.groupKDB)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
mDatabaseKDBX?.undoDeleteEntryFrom(it, parent.groupKDBX)
|
||||
}
|
||||
}
|
||||
|
||||
fun undoDeleteGroup(group: Group, parent: Group) {
|
||||
group.groupKDB?.let {
|
||||
mDatabaseKDB?.undoDeleteGroupFrom(it, parent.groupKDB)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
mDatabaseKDBX?.undoDeleteGroupFrom(it, parent.groupKDBX)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureRecycleBinExists(resources: Resources) {
|
||||
mDatabaseKDB?.ensureBackupExists()
|
||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||
@@ -1034,47 +1245,41 @@ class Database {
|
||||
}
|
||||
|
||||
fun recycle(entry: Entry, resources: Resources) {
|
||||
entry.entryKDB?.let {
|
||||
mDatabaseKDB?.recycle(it)
|
||||
ensureRecycleBinExists(resources)
|
||||
entry.parent?.let { parent ->
|
||||
removeEntryFrom(entry, parent)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
mDatabaseKDBX?.recycle(it, resources)
|
||||
recycleBin?.let {
|
||||
addEntryTo(entry, it)
|
||||
}
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(group: Group, resources: Resources) {
|
||||
group.groupKDB?.let {
|
||||
mDatabaseKDB?.recycle(it)
|
||||
ensureRecycleBinExists(resources)
|
||||
group.parent?.let { parent ->
|
||||
removeGroupFrom(group, parent)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
mDatabaseKDBX?.recycle(it, resources)
|
||||
recycleBin?.let {
|
||||
addGroupTo(group, it)
|
||||
}
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: Entry, parent: Group) {
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
parent.groupKDB?.let { parentKDB ->
|
||||
mDatabaseKDB?.undoRecycle(entryKDB, parentKDB)
|
||||
}
|
||||
}
|
||||
entry.entryKDBX?.let { entryKDBX ->
|
||||
parent.groupKDBX?.let { parentKDBX ->
|
||||
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
|
||||
}
|
||||
recycleBin?.let { it ->
|
||||
removeEntryFrom(entry, it)
|
||||
}
|
||||
addEntryTo(entry, parent)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: Group, parent: Group) {
|
||||
group.groupKDB?.let { groupKDB ->
|
||||
parent.groupKDB?.let { parentKDB ->
|
||||
mDatabaseKDB?.undoRecycle(groupKDB, parentKDB)
|
||||
}
|
||||
}
|
||||
group.groupKDBX?.let { entryKDBX ->
|
||||
parent.groupKDBX?.let { parentKDBX ->
|
||||
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
|
||||
}
|
||||
recycleBin?.let {
|
||||
removeGroupFrom(group, it)
|
||||
}
|
||||
addGroupTo(group, parent)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun startManageEntry(entry: Entry?) {
|
||||
@@ -1096,6 +1301,18 @@ class Database {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
fun allowCustomSearchableGroup(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
fun allowAutoType(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
fun allowTags(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove oldest history for each entry if more than max items or max memory
|
||||
*/
|
||||
|
||||
@@ -28,29 +28,18 @@ import java.util.*
|
||||
class DeletedObject : Parcelable {
|
||||
|
||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||
private var mDeletionTime: DateInstant? = null
|
||||
var deletionTime: DateInstant = DateInstant()
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
|
||||
this.uuid = uuid
|
||||
this.mDeletionTime = deletionTime
|
||||
this.deletionTime = deletionTime
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
|
||||
}
|
||||
|
||||
fun getDeletionTime(): DateInstant {
|
||||
if (mDeletionTime == null) {
|
||||
mDeletionTime = DateInstant(System.currentTimeMillis())
|
||||
}
|
||||
return mDeletionTime!!
|
||||
}
|
||||
|
||||
fun setDeletionTime(deletionTime: DateInstant) {
|
||||
this.mDeletionTime = deletionTime
|
||||
deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||
parcel.writeParcelable(mDeletionTime, flags)
|
||||
parcel.writeParcelable(deletionTime, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.element.entry.AutoType
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
||||
@@ -238,6 +241,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryKDBX?.notes = value
|
||||
}
|
||||
|
||||
var backgroundColor: Int?
|
||||
get() {
|
||||
var colorInt: Int? = null
|
||||
entryKDBX?.backgroundColor?.let {
|
||||
try {
|
||||
colorInt = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return colorInt
|
||||
}
|
||||
set(value) {
|
||||
entryKDBX?.backgroundColor = if (value == null) {
|
||||
""
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
}
|
||||
|
||||
var foregroundColor: Int?
|
||||
get() {
|
||||
var colorInt: Int? = null
|
||||
entryKDBX?.foregroundColor?.let {
|
||||
try {
|
||||
colorInt = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return colorInt
|
||||
}
|
||||
set(value) {
|
||||
entryKDBX?.foregroundColor = if (value == null) {
|
||||
""
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
}
|
||||
|
||||
var customData: CustomData
|
||||
get() = entryKDBX?.customData ?: CustomData()
|
||||
set(value) {
|
||||
entryKDBX?.customData = value
|
||||
}
|
||||
|
||||
var autoType: AutoType
|
||||
get() = entryKDBX?.autoType ?: AutoType()
|
||||
set(value) {
|
||||
entryKDBX?.autoType = value
|
||||
}
|
||||
|
||||
private fun isTan(): Boolean {
|
||||
return title == PMS_TAN_ENTRY && username.isNotEmpty()
|
||||
}
|
||||
@@ -419,6 +470,11 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryInfo.expiryTime = expiryTime
|
||||
entryInfo.url = url
|
||||
entryInfo.notes = notes
|
||||
entryInfo.tags = tags
|
||||
entryInfo.backgroundColor = backgroundColor
|
||||
entryInfo.foregroundColor = foregroundColor
|
||||
entryInfo.customData = customData
|
||||
entryInfo.autoType = autoType
|
||||
entryInfo.customFields = getExtraFields().toMutableList()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
@@ -453,6 +509,11 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
expiryTime = newEntryInfo.expiryTime
|
||||
url = newEntryInfo.url
|
||||
notes = newEntryInfo.notes
|
||||
tags = newEntryInfo.tags
|
||||
backgroundColor = newEntryInfo.backgroundColor
|
||||
foregroundColor = newEntryInfo.foregroundColor
|
||||
customData = newEntryInfo.customData
|
||||
autoType = newEntryInfo.autoType
|
||||
addExtraFields(newEntryInfo.customFields)
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
newEntryInfo.attachments.forEach { attachment ->
|
||||
|
||||
@@ -262,6 +262,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
var customData: CustomData
|
||||
get() = groupKDBX?.customData ?: CustomData()
|
||||
set(value) {
|
||||
groupKDBX?.customData = value
|
||||
}
|
||||
|
||||
override fun getChildGroups(): List<Group> {
|
||||
return groupKDB?.getChildGroups()?.map {
|
||||
Group(it)
|
||||
@@ -308,8 +314,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
|
||||
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
|
||||
|
||||
// TODO Change KDB parser to remove meta entries
|
||||
return groupKDB?.getChildEntries()?.filter {
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
|
||||
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
||||
}?.map {
|
||||
Entry(it)
|
||||
@@ -433,12 +440,35 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
groupKDBX?.nodeId = id
|
||||
}
|
||||
|
||||
fun setEnableAutoType(enableAutoType: Boolean?) {
|
||||
groupKDBX?.enableAutoType = enableAutoType
|
||||
var searchable: Boolean?
|
||||
get() = groupKDBX?.enableSearching
|
||||
set(value) {
|
||||
groupKDBX?.enableSearching = value
|
||||
}
|
||||
|
||||
fun setEnableSearching(enableSearching: Boolean?) {
|
||||
groupKDBX?.enableSearching = enableSearching
|
||||
fun isSearchable(): Boolean {
|
||||
val searchableGroup = searchable
|
||||
if (searchableGroup == null) {
|
||||
val parenGroup = parent
|
||||
if (parenGroup == null)
|
||||
return true
|
||||
else
|
||||
return parenGroup.isSearchable()
|
||||
} else {
|
||||
return searchableGroup
|
||||
}
|
||||
}
|
||||
|
||||
var enableAutoType: Boolean?
|
||||
get() = groupKDBX?.enableAutoType
|
||||
set(value) {
|
||||
groupKDBX?.enableAutoType = value
|
||||
}
|
||||
|
||||
var defaultAutoTypeSequence: String
|
||||
get() = groupKDBX?.defaultAutoTypeSequence ?: ""
|
||||
set(value) {
|
||||
groupKDBX?.defaultAutoTypeSequence = value
|
||||
}
|
||||
|
||||
fun setExpanded(expanded: Boolean) {
|
||||
@@ -453,6 +483,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
|
||||
fun getGroupInfo(): GroupInfo {
|
||||
val groupInfo = GroupInfo()
|
||||
groupInfo.id = groupKDBX?.nodeId?.id
|
||||
groupInfo.title = title
|
||||
groupInfo.icon = icon
|
||||
groupInfo.creationTime = creationTime
|
||||
@@ -460,6 +491,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
groupInfo.expires = expires
|
||||
groupInfo.expiryTime = expiryTime
|
||||
groupInfo.notes = notes
|
||||
groupInfo.searchable = searchable
|
||||
groupInfo.enableAutoType = enableAutoType
|
||||
groupInfo.defaultAutoTypeSequence = defaultAutoTypeSequence
|
||||
groupInfo.tags = tags
|
||||
groupInfo.customData = customData
|
||||
return groupInfo
|
||||
}
|
||||
|
||||
@@ -472,6 +508,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
expires = groupInfo.expires
|
||||
expiryTime = groupInfo.expiryTime
|
||||
notes = groupInfo.notes
|
||||
searchable = groupInfo.searchable
|
||||
enableAutoType = groupInfo.enableAutoType
|
||||
defaultAutoTypeSequence = groupInfo.defaultAutoTypeSequence
|
||||
tags = groupInfo.tags
|
||||
customData = groupInfo.customData
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -2,15 +2,19 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||
|
||||
class Tags: Parcelable {
|
||||
|
||||
private val mTags = ArrayList<String>()
|
||||
private val mTags = mutableListOf<String>()
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(values: String): this() {
|
||||
mTags.addAll(values.split(';'))
|
||||
mTags.addAll(values
|
||||
.split(DELIMITER, DELIMITER1)
|
||||
.filter { it.removeSpaceChars().isNotEmpty() }
|
||||
)
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
@@ -25,15 +29,55 @@ class Tags: Parcelable {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun setTags(tags: Tags) {
|
||||
mTags.clear()
|
||||
mTags.addAll(tags.mTags)
|
||||
}
|
||||
|
||||
fun get(position: Int): String {
|
||||
return mTags[position]
|
||||
}
|
||||
|
||||
fun put(tag: String) {
|
||||
if (tag.removeSpaceChars().isNotEmpty() && !mTags.contains(tag))
|
||||
mTags.add(tag)
|
||||
}
|
||||
|
||||
fun put(tags: Tags) {
|
||||
tags.mTags.forEach {
|
||||
put(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return mTags.isEmpty()
|
||||
}
|
||||
|
||||
fun isNotEmpty(): Boolean {
|
||||
return !isEmpty()
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return mTags.size
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mTags.clear()
|
||||
}
|
||||
|
||||
fun toList(): List<String> {
|
||||
return mTags
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return mTags.joinToString(";")
|
||||
return mTags.joinToString(DELIMITER.toString())
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Tags> {
|
||||
const val DELIMITER= ','
|
||||
const val DELIMITER1= ';'
|
||||
val DELIMITERS = listOf(',', ';')
|
||||
|
||||
override fun createFromParcel(parcel: Parcel): Tags {
|
||||
return Tags(parcel)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.binary
|
||||
|
||||
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) {
|
||||
class AttachmentPool : BinaryPool<Int>() {
|
||||
|
||||
/**
|
||||
* Utility method to find an unused key in the pool
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.util.Log
|
||||
import java.io.IOException
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
||||
abstract class BinaryPool<T> {
|
||||
|
||||
protected val pool = LinkedHashMap<T, BinaryData>()
|
||||
|
||||
@@ -225,9 +225,6 @@ abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
doForEachBinary { _, binary ->
|
||||
binary.clear(mBinaryCache)
|
||||
}
|
||||
pool.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import java.util.*
|
||||
|
||||
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
||||
class CustomIconPool : BinaryPool<UUID>() {
|
||||
|
||||
private val customIcons = HashMap<UUID, IconImageCustom>()
|
||||
|
||||
fun put(key: UUID? = null,
|
||||
name: String,
|
||||
lastModificationTime: DateInstant?,
|
||||
smallSize: Boolean,
|
||||
builder: (uniqueBinaryId: String) -> BinaryData,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
val keyBinary = super.put(key) { uniqueBinaryId ->
|
||||
// Create a byte array for better performance with small data
|
||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||
}
|
||||
val keyBinary = super.put(key, builder)
|
||||
val uuid = keyBinary.keys.first()
|
||||
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
|
||||
customIcons[uuid] = customIcon
|
||||
|
||||
@@ -34,23 +34,44 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||
|
||||
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
|
||||
EncryptionAlgorithm.AESRijndael,
|
||||
EncryptionAlgorithm.Twofish
|
||||
)
|
||||
|
||||
override var kdfEngine: KdfEngine?
|
||||
get() = kdfAvailableList[0]
|
||||
set(value) {
|
||||
value?.let {
|
||||
numberKeyEncryptionRounds = value.defaultKeyRounds
|
||||
}
|
||||
}
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine> = listOf(
|
||||
KdfFactory.aesKdf
|
||||
)
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "ISO-8859-1"
|
||||
|
||||
override var numberKeyEncryptionRounds = 300L
|
||||
|
||||
override val version: String
|
||||
get() = "V1"
|
||||
|
||||
init {
|
||||
kdfListV3.add(KdfFactory.aesKdf)
|
||||
}
|
||||
override val defaultFileExtension: String
|
||||
get() = ".kdb"
|
||||
|
||||
private fun getGroupById(groupId: Int): GroupKDB? {
|
||||
if (groupId == -1)
|
||||
return null
|
||||
return getGroupById(NodeIdInt(groupId))
|
||||
init {
|
||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||
rootGroup = createGroup().apply {
|
||||
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
val backupGroup: GroupKDB?
|
||||
@@ -63,33 +84,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return listOf(BACKUP_FOLDER_TITLE)
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine
|
||||
get() = kdfListV3[0]
|
||||
var defaultUserName: String = ""
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine>
|
||||
get() = kdfListV3
|
||||
|
||||
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<EncryptionAlgorithm>()
|
||||
list.add(EncryptionAlgorithm.AESRijndael)
|
||||
list.add(EncryptionAlgorithm.Twofish)
|
||||
return list
|
||||
}
|
||||
|
||||
val rootGroups: List<GroupKDB>
|
||||
get() {
|
||||
return rootGroup?.getChildGroups() ?: ArrayList()
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "ISO-8859-1"
|
||||
|
||||
override var numberKeyEncryptionRounds = 300L
|
||||
|
||||
init {
|
||||
algorithm = EncryptionAlgorithm.AESRijndael
|
||||
}
|
||||
var color: Int? = null
|
||||
|
||||
/**
|
||||
* Generates an unused random tree id
|
||||
@@ -215,29 +212,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return true
|
||||
}
|
||||
|
||||
fun recycle(group: GroupKDB) {
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, backupGroup)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryKDB) {
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, backupGroup)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: GroupKDB, origParent: GroupKDB) {
|
||||
removeGroupFrom(group, backupGroup)
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: EntryKDB, origParent: GroupKDB) {
|
||||
removeEntryFrom(entry, backupGroup)
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun buildNewAttachment(): BinaryData {
|
||||
fun buildNewBinaryAttachment(): BinaryData {
|
||||
// Generate an unique new file
|
||||
return attachmentPool.put { uniqueBinaryId ->
|
||||
binaryCache.getBinaryData(uniqueBinaryId, false)
|
||||
|
||||
@@ -25,16 +25,16 @@ import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.AesEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.CustomData
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.Tags
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
@@ -42,12 +42,13 @@ import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
@@ -66,6 +67,7 @@ import javax.crypto.Mac
|
||||
import javax.xml.XMLConstants
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
import kotlin.collections.HashSet
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@@ -73,27 +75,70 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
var hmacKey: ByteArray? = null
|
||||
private set
|
||||
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
|
||||
private var dataEngine: CipherEngine = AesEngine()
|
||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||
|
||||
override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||
|
||||
fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
|
||||
encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
|
||||
}
|
||||
|
||||
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
|
||||
EncryptionAlgorithm.AESRijndael,
|
||||
EncryptionAlgorithm.Twofish,
|
||||
EncryptionAlgorithm.ChaCha20
|
||||
)
|
||||
|
||||
var kdfParameters: KdfParameters? = null
|
||||
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
|
||||
override var kdfEngine: KdfEngine?
|
||||
get() = getKdfEngineFromParameters(kdfParameters)
|
||||
set(value) {
|
||||
value?.let {
|
||||
if (kdfParameters?.uuid != value.defaultParameters.uuid)
|
||||
kdfParameters = value.defaultParameters
|
||||
numberKeyEncryptionRounds = value.defaultKeyRounds
|
||||
memoryUsage = value.defaultMemoryUsage
|
||||
parallelism = value.defaultParallelism
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKdfEngineFromParameters(kdfParameters: KdfParameters?): KdfEngine? {
|
||||
if (kdfParameters == null) {
|
||||
return null
|
||||
}
|
||||
for (engine in kdfAvailableList) {
|
||||
if (engine.uuid == kdfParameters.uuid) {
|
||||
return engine
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun randomizeKdfParameters() {
|
||||
kdfParameters?.let {
|
||||
kdfEngine?.randomize(it)
|
||||
}
|
||||
}
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine> = listOf(
|
||||
KdfFactory.aesKdf,
|
||||
KdfFactory.argon2dKdf,
|
||||
KdfFactory.argon2idKdf
|
||||
)
|
||||
|
||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||
|
||||
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||
private val mTemplateEngine = TemplateEngineCompatible(this)
|
||||
|
||||
var kdbxVersion = UnsignedInt(0)
|
||||
var name = ""
|
||||
var nameChanged = DateInstant()
|
||||
// TODO change setting date
|
||||
var settingsChanged = DateInstant()
|
||||
var description = ""
|
||||
var descriptionChanged = DateInstant()
|
||||
var defaultUserName = ""
|
||||
var defaultUserNameChanged = DateInstant()
|
||||
|
||||
// TODO last change date
|
||||
var settingsChanged = DateInstant()
|
||||
var keyLastChanged = DateInstant()
|
||||
var keyChangeRecDays: Long = -1
|
||||
var keyChangeForceDays: Long = 1
|
||||
@@ -115,16 +160,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
var lastSelectedGroupUUID = UUID_ZERO
|
||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||
var memoryProtection = MemoryProtectionConfig()
|
||||
val deletedObjects = ArrayList<DeletedObject>()
|
||||
val deletedObjects = HashSet<DeletedObject>()
|
||||
var publicCustomData = VariantDictionary()
|
||||
val customData = CustomData()
|
||||
|
||||
var localizedAppName = "KeePassDX"
|
||||
val tagPool = Tags()
|
||||
|
||||
init {
|
||||
kdfList.add(KdfFactory.aesKdf)
|
||||
kdfList.add(KdfFactory.argon2dKdf)
|
||||
kdfList.add(KdfFactory.argon2idKdf)
|
||||
}
|
||||
var localizedAppName = "KeePassDX"
|
||||
|
||||
constructor()
|
||||
|
||||
@@ -159,38 +201,75 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return "V2 - KDBX$kdbxStringVersion"
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = try {
|
||||
getEngineKDBX4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
null
|
||||
override val defaultFileExtension: String
|
||||
get() = ".kdbx"
|
||||
|
||||
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
|
||||
var containsCustomData = false
|
||||
override fun operate(node: T): Boolean {
|
||||
if (node.customData.isNotEmpty()) {
|
||||
containsCustomData = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine>
|
||||
get() = kdfList
|
||||
|
||||
@Throws(UnknownKDF::class)
|
||||
fun getEngineKDBX4(kdfParameters: KdfParameters?): KdfEngine {
|
||||
val unknownKDFException = UnknownKDF()
|
||||
if (kdfParameters == null) {
|
||||
throw unknownKDFException
|
||||
private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
|
||||
var passwordQualityEstimationDisabled = false
|
||||
override fun operate(node: EntryKDBX): Boolean {
|
||||
if (!node.qualityCheck) {
|
||||
passwordQualityEstimationDisabled = true
|
||||
}
|
||||
for (engine in kdfList) {
|
||||
if (engine.uuid == kdfParameters.uuid) {
|
||||
return engine
|
||||
return super.operate(node)
|
||||
}
|
||||
}
|
||||
throw unknownKDFException
|
||||
}
|
||||
|
||||
val availableCompressionAlgorithms: List<CompressionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<CompressionAlgorithm>()
|
||||
list.add(CompressionAlgorithm.None)
|
||||
list.add(CompressionAlgorithm.GZip)
|
||||
return list
|
||||
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
|
||||
var containsTags = false
|
||||
override fun operate(node: GroupKDBX): Boolean {
|
||||
if (node.tags.isNotEmpty())
|
||||
containsTags = true
|
||||
return super.operate(node)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMinKdbxVersion(): UnsignedInt {
|
||||
val entryHandler = EntryOperationHandler()
|
||||
val groupHandler = GroupOperationHandler()
|
||||
rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
|
||||
|
||||
// https://keepass.info/help/kb/kdbx_4.1.html
|
||||
val containsGroupWithTag = groupHandler.containsTags
|
||||
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
|
||||
val containsCustomIconWithNameOrLastModificationTime = iconsManager.containsCustomIconWithNameOrLastModificationTime()
|
||||
val containsHeaderCustomDataWithLastModificationTime = customData.containsItemWithLastModificationTime()
|
||||
|
||||
// https://keepass.info/help/kb/kdbx_4.html
|
||||
// If AES is not use, it's at least 4.0
|
||||
val keyDerivationFunction = kdfEngine
|
||||
val kdfIsNotAes = keyDerivationFunction != null && keyDerivationFunction.uuid != AesKdf.CIPHER_UUID
|
||||
val containsHeaderCustomData = customData.isNotEmpty()
|
||||
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
|
||||
|
||||
// Check each condition to determine version
|
||||
return if (containsGroupWithTag
|
||||
|| containsEntryWithPasswordQualityEstimationDisabled
|
||||
|| containsCustomIconWithNameOrLastModificationTime
|
||||
|| containsHeaderCustomDataWithLastModificationTime) {
|
||||
FILE_VERSION_41
|
||||
} else if (kdfIsNotAes
|
||||
|| containsHeaderCustomData
|
||||
|| containsNodeCustomData) {
|
||||
FILE_VERSION_40
|
||||
} else {
|
||||
FILE_VERSION_31
|
||||
}
|
||||
}
|
||||
|
||||
val availableCompressionAlgorithms: List<CompressionAlgorithm> = listOf(
|
||||
CompressionAlgorithm.None,
|
||||
CompressionAlgorithm.GZip
|
||||
)
|
||||
|
||||
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
@@ -245,18 +324,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
}
|
||||
|
||||
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<EncryptionAlgorithm>()
|
||||
list.add(EncryptionAlgorithm.AESRijndael)
|
||||
list.add(EncryptionAlgorithm.Twofish)
|
||||
list.add(EncryptionAlgorithm.ChaCha20)
|
||||
return list
|
||||
}
|
||||
|
||||
override var numberKeyEncryptionRounds: Long
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
var numKeyEncRounds: Long = 0
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
|
||||
return numKeyEncRounds
|
||||
@@ -265,7 +336,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
|
||||
numKeyEncRounds = rounds
|
||||
}
|
||||
|
||||
var memoryUsage: Long
|
||||
@@ -305,7 +375,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
// Retrieve recycle bin in index
|
||||
val recycleBin: GroupKDBX?
|
||||
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID)
|
||||
get() = getGroupByUUID(recycleBinUUID)
|
||||
|
||||
val lastSelectedGroup: GroupKDBX?
|
||||
get() = getGroupByUUID(lastSelectedGroupUUID)
|
||||
@@ -313,17 +383,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val lastTopVisibleGroup: GroupKDBX?
|
||||
get() = getGroupByUUID(lastTopVisibleGroupUUID)
|
||||
|
||||
fun setDataEngine(dataEngine: CipherEngine) {
|
||||
this.dataEngine = dataEngine
|
||||
}
|
||||
|
||||
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||
return this.iconsManager.getIcon(iconId)
|
||||
}
|
||||
|
||||
fun buildNewCustomIcon(customIconId: UUID? = null,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
iconsManager.buildNewCustomIcon(customIconId, result)
|
||||
// Create a binary file for a brand new custom icon
|
||||
addCustomIcon(customIconId, "", null, false, result)
|
||||
}
|
||||
|
||||
fun addCustomIcon(customIconId: UUID? = null,
|
||||
@@ -331,14 +398,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
lastModificationTime: DateInstant?,
|
||||
smallSize: Boolean,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
|
||||
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, { uniqueBinaryId ->
|
||||
// Create a byte array for better performance with small data
|
||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||
}, result)
|
||||
}
|
||||
|
||||
fun removeCustomIcon(iconUuid: UUID) {
|
||||
iconsManager.removeCustomIcon(iconUuid, binaryCache)
|
||||
}
|
||||
|
||||
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||
return iconsManager.isCustomIconBinaryDuplicate(binary)
|
||||
}
|
||||
|
||||
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
|
||||
fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
|
||||
return this.iconsManager.getIcon(iconUuid)
|
||||
}
|
||||
|
||||
@@ -355,7 +429,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val templatesGroup = firstGroupWithValidName
|
||||
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||
entryTemplatesGroup = templatesGroup.id
|
||||
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
||||
} else {
|
||||
removeTemplatesGroup()
|
||||
}
|
||||
@@ -363,7 +436,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
fun removeTemplatesGroup() {
|
||||
entryTemplatesGroup = UUID_ZERO
|
||||
entryTemplatesGroupChanged = DateInstant()
|
||||
mTemplateEngine.clearCache()
|
||||
}
|
||||
|
||||
@@ -414,37 +486,37 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
|
||||
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeTitleKey(recursionLevel).equals(title, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeUsernameKey(recursionLevel).equals(username, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeUrlKey(recursionLevel).equals(url, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodePasswordKey(recursionLevel).equals(password, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.decodeNotesKey(recursionLevel).equals(notes, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
||||
return entryIndexes.values.find { entry ->
|
||||
return findEntry { entry ->
|
||||
entry.customData.containsItemWithValue(customDataValue)
|
||||
}
|
||||
}
|
||||
@@ -476,7 +548,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
kdfParameters?.let { keyDerivationFunctionParameters ->
|
||||
val kdfEngine = getEngineKDBX4(keyDerivationFunctionParameters)
|
||||
val kdfEngine = getKdfEngineFromParameters(keyDerivationFunctionParameters)
|
||||
?: throw IOException("Unknown key derivation function")
|
||||
|
||||
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||
if (transformedMasterKey.size != 32) {
|
||||
@@ -486,7 +559,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val cmpKey = ByteArray(65)
|
||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||
finalKey = resizeKey(cmpKey, dataEngine.keyLength())
|
||||
finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
|
||||
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
@@ -724,14 +797,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
firstGroupWithValidName
|
||||
}
|
||||
recycleBinUUID = recycleBinGroup.id
|
||||
recycleBinChanged = recycleBinGroup.lastModificationTime
|
||||
recycleBinChanged = DateInstant()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRecycleBin() {
|
||||
if (recycleBin != null) {
|
||||
recycleBinUUID = UUID_ZERO
|
||||
recycleBinChanged = DateInstant()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,64 +825,41 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return false
|
||||
}
|
||||
|
||||
fun recycle(group: GroupKDBX, resources: Resources) {
|
||||
ensureRecycleBinExists(resources)
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, recycleBin)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryKDBX, resources: Resources) {
|
||||
ensureRecycleBinExists(resources)
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, recycleBin)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: GroupKDBX, origParent: GroupKDBX) {
|
||||
removeGroupFrom(group, recycleBin)
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: EntryKDBX, origParent: GroupKDBX) {
|
||||
removeEntryFrom(entry, recycleBin)
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun getDeletedObjects(): List<DeletedObject> {
|
||||
return deletedObjects
|
||||
fun getDeletedObject(nodeId: NodeId<UUID>): DeletedObject? {
|
||||
return deletedObjects.find { it.uuid == nodeId.id }
|
||||
}
|
||||
|
||||
fun addDeletedObject(deletedObject: DeletedObject) {
|
||||
this.deletedObjects.add(deletedObject)
|
||||
}
|
||||
|
||||
fun addDeletedObject(objectId: UUID) {
|
||||
addDeletedObject(DeletedObject(objectId))
|
||||
}
|
||||
|
||||
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
||||
super.addEntryTo(newEntry, parent)
|
||||
tagPool.put(newEntry.tags)
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun updateEntry(entry: EntryKDBX) {
|
||||
super.updateEntry(entry)
|
||||
tagPool.put(entry.tags)
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
||||
super.removeEntryFrom(entryToRemove, parent)
|
||||
deletedObjects.add(DeletedObject(entryToRemove.id))
|
||||
// Do not remove tags from pool, it's only in temp memory
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
||||
super.undoDeleteEntryFrom(entry, origParent)
|
||||
deletedObjects.remove(DeletedObject(entry.id))
|
||||
}
|
||||
|
||||
fun containsPublicCustomData(): Boolean {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
fun buildNewAttachment(smallSize: Boolean,
|
||||
fun buildNewBinaryAttachment(smallSize: Boolean,
|
||||
compression: Boolean,
|
||||
protection: Boolean,
|
||||
binaryPoolId: Int? = null): BinaryData {
|
||||
@@ -830,6 +879,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
|
||||
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
|
||||
// TODO check in icon pool
|
||||
// Build binaries to remove with all binaries known
|
||||
val binariesToRemove = ArrayList<BinaryData>()
|
||||
if (binaries.isEmpty()) {
|
||||
@@ -866,11 +916,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return super.validatePasswordEncoding(password, containsKeyFile)
|
||||
}
|
||||
|
||||
override fun clearCache() {
|
||||
override fun clearIndexes() {
|
||||
try {
|
||||
super.clearCache()
|
||||
super.clearIndexes()
|
||||
mFieldReferenceEngine.clear()
|
||||
attachmentPool.clear()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear cache", e)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
@@ -44,51 +46,43 @@ abstract class DatabaseVersioned<
|
||||
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
|
||||
> {
|
||||
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
protected var algorithm: EncryptionAlgorithm? = null
|
||||
abstract var encryptionAlgorithm: EncryptionAlgorithm
|
||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
|
||||
abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine?
|
||||
abstract var kdfEngine: KdfEngine?
|
||||
abstract val kdfAvailableList: List<KdfEngine>
|
||||
abstract var numberKeyEncryptionRounds: Long
|
||||
|
||||
abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine>
|
||||
protected abstract val passwordEncoding: String
|
||||
|
||||
var masterKey = ByteArray(32)
|
||||
var finalKey: ByteArray? = null
|
||||
protected set
|
||||
|
||||
abstract val version: String
|
||||
abstract val defaultFileExtension: String
|
||||
|
||||
/**
|
||||
* To manage binaries in faster way
|
||||
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||
* Can be used to temporarily store database elements
|
||||
*/
|
||||
var binaryCache = BinaryCache()
|
||||
val iconsManager = IconsManager(binaryCache)
|
||||
var attachmentPool = AttachmentPool(binaryCache)
|
||||
var iconsManager = IconsManager()
|
||||
var attachmentPool = AttachmentPool()
|
||||
|
||||
var changeDuplicateId = false
|
||||
|
||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||
|
||||
abstract val version: String
|
||||
|
||||
protected abstract val passwordEncoding: String
|
||||
|
||||
abstract var numberKeyEncryptionRounds: Long
|
||||
|
||||
var encryptionAlgorithm: EncryptionAlgorithm
|
||||
get() {
|
||||
return algorithm ?: EncryptionAlgorithm.AESRijndael
|
||||
}
|
||||
set(algorithm) {
|
||||
this.algorithm = algorithm
|
||||
}
|
||||
|
||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||
|
||||
var rootGroup: Group? = null
|
||||
set(value) {
|
||||
field = value
|
||||
value?.let {
|
||||
removeGroupIndex(it)
|
||||
addGroupIndex(it)
|
||||
}
|
||||
}
|
||||
@@ -198,12 +192,6 @@ abstract class DatabaseVersioned<
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
fun doForEachGroupInIndex(action: (Group) -> Unit) {
|
||||
for (group in groupIndexes) {
|
||||
action.invoke(group.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an id number is already in use
|
||||
*
|
||||
@@ -219,14 +207,7 @@ abstract class DatabaseVersioned<
|
||||
return groupIndexes.values
|
||||
}
|
||||
|
||||
fun setGroupIndexes(groupList: List<Group>) {
|
||||
this.groupIndexes.clear()
|
||||
for (currentGroup in groupList) {
|
||||
this.groupIndexes[currentGroup.nodeId] = currentGroup
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupById(id: NodeId<GroupId>): Group? {
|
||||
open fun getGroupById(id: NodeId<GroupId>): Group? {
|
||||
return this.groupIndexes[id]
|
||||
}
|
||||
|
||||
@@ -250,16 +231,6 @@ abstract class DatabaseVersioned<
|
||||
this.groupIndexes.remove(group.nodeId)
|
||||
}
|
||||
|
||||
fun numberOfGroups(): Int {
|
||||
return groupIndexes.size
|
||||
}
|
||||
|
||||
fun doForEachEntryInIndex(action: (Entry) -> Unit) {
|
||||
for (entry in entryIndexes) {
|
||||
action.invoke(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEntryIdUsed(id: NodeId<EntryId>): Boolean {
|
||||
return entryIndexes.containsKey(id)
|
||||
}
|
||||
@@ -272,6 +243,10 @@ abstract class DatabaseVersioned<
|
||||
return this.entryIndexes[id]
|
||||
}
|
||||
|
||||
fun findEntry(predicate: (Entry) -> Boolean): Entry? {
|
||||
return this.entryIndexes.values.find(predicate)
|
||||
}
|
||||
|
||||
fun addEntryIndex(entry: Entry) {
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
@@ -292,11 +267,7 @@ abstract class DatabaseVersioned<
|
||||
this.entryIndexes.remove(entry.nodeId)
|
||||
}
|
||||
|
||||
fun numberOfEntries(): Int {
|
||||
return entryIndexes.size
|
||||
}
|
||||
|
||||
open fun clearCache() {
|
||||
open fun clearIndexes() {
|
||||
this.groupIndexes.clear()
|
||||
this.entryIndexes.clear()
|
||||
}
|
||||
@@ -326,7 +297,7 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
open fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
// Remove tree from parent tree
|
||||
parent?.removeChildGroup(groupToRemove)
|
||||
removeGroupIndex(groupToRemove)
|
||||
@@ -353,23 +324,39 @@ abstract class DatabaseVersioned<
|
||||
removeEntryIndex(entryToRemove)
|
||||
}
|
||||
|
||||
// TODO Delete group
|
||||
fun undoDeleteGroupFrom(group: Group, origParent: Group?) {
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
open fun undoDeleteEntryFrom(entry: Entry, origParent: Group?) {
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
abstract fun isInRecycleBin(group: Group): Boolean
|
||||
|
||||
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
|
||||
if (group == null)
|
||||
return false
|
||||
if (omitBackup && isInRecycleBin(group))
|
||||
return false
|
||||
return true
|
||||
fun clearIconsCache() {
|
||||
iconsManager.doForEachCustomIcon { _, binary ->
|
||||
try {
|
||||
binary.clear(binaryCache)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear icon binary cache", e)
|
||||
}
|
||||
}
|
||||
iconsManager.clear()
|
||||
}
|
||||
|
||||
fun clearAttachmentsCache() {
|
||||
attachmentPool.doForEachBinary { _, binary ->
|
||||
try {
|
||||
binary.clear(binaryCache)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear attachment binary cache", e)
|
||||
}
|
||||
}
|
||||
attachmentPool.clear()
|
||||
}
|
||||
|
||||
fun clearBinaries() {
|
||||
binaryCache.clear()
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
clearIndexes()
|
||||
clearIconsCache()
|
||||
clearAttachmentsCache()
|
||||
clearBinaries()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
@@ -60,8 +61,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
private var binaryDataId: Int? = null
|
||||
|
||||
// Determine if this is a MetaStream entry
|
||||
val isMetaStream: Boolean
|
||||
get() {
|
||||
fun isMetaStream(): Boolean {
|
||||
if (notes.isEmpty()) return false
|
||||
if (binaryDescription != PMS_ID_BINDESC) return false
|
||||
if (title.isEmpty()) return false
|
||||
@@ -73,6 +73,32 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
return icon.standard.id == KEY_ID
|
||||
}
|
||||
|
||||
fun isMetaStreamDefaultUsername(): Boolean {
|
||||
return isMetaStream() && notes == PMS_STREAM_DEFAULTUSER
|
||||
}
|
||||
|
||||
private fun setMetaStream() {
|
||||
binaryDescription = PMS_ID_BINDESC
|
||||
title = PMS_ID_TITLE
|
||||
username = PMS_ID_USER
|
||||
url = PMS_ID_URL
|
||||
icon.standard = IconImageStandard(KEY_ID)
|
||||
}
|
||||
|
||||
fun setMetaStreamDefaultUsername() {
|
||||
notes = PMS_STREAM_DEFAULTUSER
|
||||
setMetaStream()
|
||||
}
|
||||
|
||||
fun isMetaStreamDatabaseColor(): Boolean {
|
||||
return isMetaStream() && notes == PMS_STREAM_DBCOLOR
|
||||
}
|
||||
|
||||
fun setMetaStreamDatabaseColor() {
|
||||
notes = PMS_STREAM_DBCOLOR
|
||||
setMetaStream()
|
||||
}
|
||||
|
||||
override fun initNodeId(): NodeId<UUID> {
|
||||
return NodeIdUUID()
|
||||
}
|
||||
@@ -113,8 +139,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
dest.writeInt(binaryDataId ?: -1)
|
||||
}
|
||||
|
||||
fun updateWith(source: EntryKDB) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: EntryKDB,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
title = source.title
|
||||
username = source.username
|
||||
password = source.password
|
||||
@@ -184,6 +211,13 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
private const val PMS_ID_USER = "SYSTEM"
|
||||
private const val PMS_ID_URL = "$"
|
||||
|
||||
const val PMS_STREAM_SIMPLESTATE = "Simple UI State"
|
||||
const val PMS_STREAM_DEFAULTUSER = "Default User Name"
|
||||
const val PMS_STREAM_SEARCHHISTORYITEM = "Search History Item"
|
||||
const val PMS_STREAM_CUSTOMKVP = "Custom KVP"
|
||||
const val PMS_STREAM_DBCOLOR = "Database Color"
|
||||
const val PMS_STREAM_KPXICON2 = "KPX_CUSTOM_ICONS_2"
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryKDB {
|
||||
|
||||
@@ -110,8 +110,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
* Update with deep copy of each entry element
|
||||
* @param source
|
||||
*/
|
||||
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: EntryKDBX,
|
||||
copyHistory: Boolean = true,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
usageCount = source.usageCount
|
||||
locationChanged = DateInstant(source.locationChanged)
|
||||
customData = CustomData(source.customData)
|
||||
|
||||
@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
dest.writeInt(groupFlags)
|
||||
}
|
||||
|
||||
fun updateWith(source: GroupKDB) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: GroupKDB,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
groupFlags = source.groupFlags
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
override var customData = CustomData()
|
||||
var notes = ""
|
||||
var isExpanded = true
|
||||
var defaultAutoTypeSequence = ""
|
||||
var enableAutoType: Boolean? = null
|
||||
var enableSearching: Boolean? = null
|
||||
var enableAutoType: Boolean? = null
|
||||
var defaultAutoTypeSequence: String = ""
|
||||
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
|
||||
override var tags = Tags()
|
||||
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
|
||||
@@ -69,11 +69,11 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
|
||||
notes = parcel.readString() ?: notes
|
||||
isExpanded = parcel.readByte().toInt() != 0
|
||||
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
|
||||
val isAutoTypeEnabled = parcel.readInt()
|
||||
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
|
||||
val isSearchingEnabled = parcel.readInt()
|
||||
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
|
||||
val isAutoTypeEnabled = parcel.readInt()
|
||||
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
|
||||
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
|
||||
lastTopVisibleEntry = parcel.readSerializable() as UUID
|
||||
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
|
||||
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||
@@ -94,25 +94,26 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
dest.writeParcelable(customData, flags)
|
||||
dest.writeString(notes)
|
||||
dest.writeByte((if (isExpanded) 1 else 0).toByte())
|
||||
dest.writeString(defaultAutoTypeSequence)
|
||||
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
|
||||
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
|
||||
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
|
||||
dest.writeString(defaultAutoTypeSequence)
|
||||
dest.writeSerializable(lastTopVisibleEntry)
|
||||
dest.writeParcelable(tags, flags)
|
||||
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
||||
}
|
||||
|
||||
fun updateWith(source: GroupKDBX) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: GroupKDBX,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
usageCount = source.usageCount
|
||||
locationChanged = DateInstant(source.locationChanged)
|
||||
// Add all custom elements in map
|
||||
customData = CustomData(source.customData)
|
||||
notes = source.notes
|
||||
isExpanded = source.isExpanded
|
||||
defaultAutoTypeSequence = source.defaultAutoTypeSequence
|
||||
enableAutoType = source.enableAutoType
|
||||
enableSearching = source.enableSearching
|
||||
enableAutoType = source.enableAutoType
|
||||
defaultAutoTypeSequence = source.defaultAutoTypeSequence
|
||||
lastTopVisibleEntry = source.lastTopVisibleEntry
|
||||
tags = source.tags
|
||||
previousParentGroup = source.previousParentGroup
|
||||
|
||||
@@ -51,13 +51,16 @@ abstract class GroupVersioned
|
||||
dest.writeString(titleGroup)
|
||||
}
|
||||
|
||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
|
||||
super.updateWith(source)
|
||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
titleGroup = source.titleGroup
|
||||
if (updateParents) {
|
||||
removeChildren()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.addAll(source.childEntries)
|
||||
}
|
||||
}
|
||||
|
||||
override var title: String
|
||||
get() = titleGroup
|
||||
|
||||
@@ -81,6 +81,7 @@ class IconImageStandard : IconImageDraw {
|
||||
const val CREDIT_CARD_ID = 37
|
||||
const val TRASH_ID = 43
|
||||
const val FOLDER_ID = 48
|
||||
const val DATABASE_ID = 50
|
||||
const val LIST_ID = 57
|
||||
const val BUILD_ID = 59
|
||||
const val STAR_ID = 61
|
||||
|
||||
@@ -28,12 +28,12 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
|
||||
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||
import java.util.*
|
||||
|
||||
class IconsManager(binaryCache: BinaryCache) {
|
||||
class IconsManager {
|
||||
|
||||
private val standardCache = List(NB_ICONS) {
|
||||
IconImageStandard(it)
|
||||
}
|
||||
private val customCache = CustomIconPool(binaryCache)
|
||||
private val customCache = CustomIconPool()
|
||||
|
||||
fun getIcon(iconId: Int): IconImageStandard {
|
||||
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
||||
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
|
||||
* Custom
|
||||
*/
|
||||
|
||||
fun buildNewCustomIcon(key: UUID? = null,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
// Create a binary file for a brand new custom icon
|
||||
addCustomIcon(key, "", null, false, result)
|
||||
}
|
||||
|
||||
fun addCustomIcon(key: UUID? = null,
|
||||
name: String,
|
||||
lastModificationTime: DateInstant?,
|
||||
smallSize: Boolean,
|
||||
builder: (uniqueBinaryId: String) -> BinaryData,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
customCache.put(key, name, lastModificationTime, smallSize, result)
|
||||
customCache.put(key, name, lastModificationTime, builder, result)
|
||||
}
|
||||
|
||||
fun getIcon(iconUuid: UUID): IconImageCustom {
|
||||
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
|
||||
fun getIcon(iconUuid: UUID): IconImageCustom? {
|
||||
return customCache.getCustomIcon(iconUuid)
|
||||
}
|
||||
|
||||
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||
return customCache.isBinaryDuplicate(binaryData)
|
||||
}
|
||||
|
||||
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) {
|
||||
fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
|
||||
val binary = customCache[iconUuid]
|
||||
customCache.remove(iconUuid)
|
||||
try {
|
||||
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
|
||||
/**
|
||||
* Clear the cache of icons
|
||||
*/
|
||||
fun clearCache() {
|
||||
try {
|
||||
fun clear() {
|
||||
customCache.clear()
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to clear cache", e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface<Group> {
|
||||
fun removeParent() {
|
||||
parent = null
|
||||
}
|
||||
|
||||
fun getPathString(): String {
|
||||
val pathNodes = mutableListOf<Node>()
|
||||
var currentNode = this
|
||||
pathNodes.add(0, currentNode)
|
||||
while (currentNode.containsParent()) {
|
||||
currentNode.parent?.let { parent ->
|
||||
currentNode = parent
|
||||
pathNodes.add(0, currentNode)
|
||||
}
|
||||
}
|
||||
return pathNodes.joinToString("/") { it.title }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,4 +44,6 @@ abstract class NodeId<Id> : Parcelable {
|
||||
override fun hashCode(): Int {
|
||||
return id?.hashCode() ?: 0
|
||||
}
|
||||
|
||||
abstract fun toVisualString(): String?
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ class NodeIdInt : NodeId<Int> {
|
||||
return id.toString()
|
||||
}
|
||||
|
||||
override fun toVisualString(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> {
|
||||
|
||||
@@ -64,6 +64,10 @@ class NodeIdUUID : NodeId<UUID> {
|
||||
return UuidUtil.toHexString(id) ?: id.toString()
|
||||
}
|
||||
|
||||
override fun toVisualString(): String {
|
||||
return toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> {
|
||||
|
||||
@@ -68,9 +68,12 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
||||
return 0
|
||||
}
|
||||
|
||||
protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>) {
|
||||
protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>,
|
||||
updateParents: Boolean = true) {
|
||||
this.nodeId = copyNodeId(source.nodeId)
|
||||
if (updateParents) {
|
||||
this.parent = source.parent
|
||||
}
|
||||
this.icon = source.icon
|
||||
this.creationTime = DateInstant(source.creationTime)
|
||||
this.lastModificationTime = DateInstant(source.lastModificationTime)
|
||||
|
||||
@@ -23,10 +23,8 @@ import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class Template : Parcelable {
|
||||
|
||||
@@ -34,6 +32,8 @@ class Template : Parcelable {
|
||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||
var title = ""
|
||||
var icon = IconImage()
|
||||
var backgroundColor: Int? = null
|
||||
var foregroundColor: Int? = null
|
||||
var sections: MutableList<TemplateSection> = ArrayList()
|
||||
private set
|
||||
|
||||
@@ -41,7 +41,8 @@ class Template : Parcelable {
|
||||
title: String,
|
||||
icon: IconImage,
|
||||
section: TemplateSection,
|
||||
version: Int = 1): this(uuid, title, icon, ArrayList<TemplateSection>().apply {
|
||||
version: Int = 1)
|
||||
: this(uuid, title, icon, ArrayList<TemplateSection>().apply {
|
||||
add(section)
|
||||
}, version)
|
||||
|
||||
@@ -49,11 +50,22 @@ class Template : Parcelable {
|
||||
title: String,
|
||||
icon: IconImage,
|
||||
sections: List<TemplateSection>,
|
||||
version: Int = 1)
|
||||
: this(uuid, title, icon, null, null, sections, version)
|
||||
|
||||
constructor(uuid: UUID,
|
||||
title: String,
|
||||
icon: IconImage,
|
||||
backgroundColor: Int?,
|
||||
foregroundColor: Int?,
|
||||
sections: List<TemplateSection>,
|
||||
version: Int = 1) {
|
||||
this.version = version
|
||||
this.uuid = uuid
|
||||
this.title = title
|
||||
this.icon = icon
|
||||
this.backgroundColor = backgroundColor
|
||||
this.foregroundColor = foregroundColor
|
||||
this.sections.clear()
|
||||
this.sections.addAll(sections)
|
||||
}
|
||||
@@ -63,6 +75,8 @@ class Template : Parcelable {
|
||||
this.uuid = template.uuid
|
||||
this.title = template.title
|
||||
this.icon = template.icon
|
||||
this.backgroundColor = template.backgroundColor
|
||||
this.foregroundColor = template.foregroundColor
|
||||
this.sections.clear()
|
||||
this.sections.addAll(template.sections)
|
||||
}
|
||||
@@ -72,6 +86,8 @@ class Template : Parcelable {
|
||||
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
|
||||
title = parcel.readString() ?: title
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||
backgroundColor = parcel.readInt()
|
||||
foregroundColor = parcel.readInt()
|
||||
parcel.readList(sections, TemplateSection::class.java.classLoader)
|
||||
}
|
||||
|
||||
@@ -80,6 +96,8 @@ class Template : Parcelable {
|
||||
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||
parcel.writeString(title)
|
||||
parcel.writeParcelable(icon, flags)
|
||||
parcel.writeInt(backgroundColor ?: -1)
|
||||
parcel.writeInt(foregroundColor ?: -1)
|
||||
parcel.writeList(sections)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
enum class TemplateAttributeAction {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.os.Parcel
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class TemplateBuilder {
|
||||
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
@@ -26,7 +44,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
||||
if (templateGroup != null) {
|
||||
templates.add(Template.STANDARD)
|
||||
templateGroup.getChildEntries().forEach { templateEntry ->
|
||||
getTemplateFromTemplateEntry(templateEntry)?.let {
|
||||
getTemplateFromTemplateEntry(templateEntry).let {
|
||||
mCacheTemplates[templateEntry.id] = it
|
||||
templates.add(it)
|
||||
}
|
||||
@@ -70,7 +88,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
||||
return mCacheTemplates[uuid]
|
||||
else {
|
||||
mDatabase.getEntryById(uuid)?.let { templateEntry ->
|
||||
getTemplateFromTemplateEntry(templateEntry)?.let { newTemplate ->
|
||||
getTemplateFromTemplateEntry(templateEntry).let { newTemplate ->
|
||||
mCacheTemplates[uuid] = newTemplate
|
||||
return newTemplate
|
||||
}
|
||||
@@ -134,7 +152,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
||||
return TemplateSection(sectionAttributes)
|
||||
}
|
||||
|
||||
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template? {
|
||||
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template {
|
||||
|
||||
val templateEntryDecoded = decodeTemplateEntry(templateEntry)
|
||||
val templateSections = mutableListOf<TemplateSection>()
|
||||
@@ -149,7 +167,28 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
||||
}
|
||||
templateSections.add(buildTemplateSectionFromFields(sectionFields))
|
||||
|
||||
return Template(templateEntry.id, templateEntry.title, templateEntry.icon, templateSections, getVersion())
|
||||
var backgroundColor: Int? = null
|
||||
templateEntry.backgroundColor.let {
|
||||
try {
|
||||
backgroundColor = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
var foregroundColor: Int? = null
|
||||
templateEntry.foregroundColor.let {
|
||||
try {
|
||||
foregroundColor = Color.parseColor(it)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
|
||||
return Template(
|
||||
templateEntry.id,
|
||||
templateEntry.title,
|
||||
templateEntry.icon,
|
||||
backgroundColor,
|
||||
foregroundColor,
|
||||
templateSections,
|
||||
getVersion()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.util.Log
|
||||
@@ -257,6 +275,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
entryCopy.putField(field)
|
||||
}
|
||||
}
|
||||
// Add colors
|
||||
entryCopy.foregroundColor = templateEntry.foregroundColor
|
||||
entryCopy.backgroundColor = templateEntry.backgroundColor
|
||||
|
||||
return entryCopy
|
||||
}
|
||||
@@ -359,6 +380,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add colors
|
||||
entryCopy.foregroundColor = templateEntry.foregroundColor
|
||||
entryCopy.backgroundColor = templateEntry.backgroundColor
|
||||
return entryCopy
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.content.Context
|
||||
@@ -85,7 +103,7 @@ object TemplateField {
|
||||
LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid)
|
||||
LABEL_TYPE.equals(name, true) -> context.getString(R.string.type)
|
||||
LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency)
|
||||
LABEL_TOKEN.equals(name, true) -> context.getString(R.string.token)
|
||||
LABEL_TOKEN.equals(name, false) -> context.getString(R.string.token)
|
||||
LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key)
|
||||
LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key)
|
||||
LABEL_SEED.equals(name, true) -> context.getString(R.string.seed)
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.os.Parcel
|
||||
|
||||
@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
}
|
||||
|
||||
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.file_not_found_content
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file
|
||||
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
|
||||
abstract class DatabaseHeader {
|
||||
|
||||
/**
|
||||
@@ -33,8 +31,4 @@ abstract class DatabaseHeader {
|
||||
*/
|
||||
var encryptionIV = ByteArray(16)
|
||||
|
||||
companion object {
|
||||
val PWM_DBSIG_1 = UnsignedInt(-0x655d26fd)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
*/
|
||||
var transformSeed = ByteArray(32)
|
||||
|
||||
var signature1 = UnsignedInt(0) // = PWM_DBSIG_1
|
||||
var signature1 = UnsignedInt(0) // = DBSIG_1
|
||||
var signature2 = UnsignedInt(0) // = DBSIG_2
|
||||
var flags= UnsignedInt(0)
|
||||
var version= UnsignedInt(0)
|
||||
@@ -84,9 +84,9 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
companion object {
|
||||
|
||||
// DB sig from KeePass 1.03
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab4049b)
|
||||
// DB sig from KeePass 1.03
|
||||
val DBVER_DW = UnsignedInt(0x00030003)
|
||||
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab4049b) // 0xB54BFB65
|
||||
val DBVER_DW = UnsignedInt(0x00030004)
|
||||
|
||||
val FLAG_SHA2 = UnsignedInt(1)
|
||||
val FLAG_RIJNDAEL = UnsignedInt(2)
|
||||
@@ -97,7 +97,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
||||
const val BUF_SIZE = 124
|
||||
|
||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||
return sig1.toKotlinInt() == PWM_DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
|
||||
return sig1.toKotlinInt() == DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
|
||||
}
|
||||
|
||||
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.database.file
|
||||
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
||||
@@ -28,9 +27,6 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||
import com.kunzisoft.keepass.stream.CopyInputStream
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
@@ -87,71 +83,10 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
inner class HeaderAndHash(var header: ByteArray, var hash: ByteArray)
|
||||
|
||||
init {
|
||||
this.version = getMinKdbxVersion(databaseV4) // Only for writing
|
||||
this.version = databaseV4.getMinKdbxVersion()
|
||||
this.masterSeed = ByteArray(32)
|
||||
}
|
||||
|
||||
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
|
||||
var containsCustomData = false
|
||||
override fun operate(node: T): Boolean {
|
||||
if (node.customData.isNotEmpty()) {
|
||||
containsCustomData = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
|
||||
var passwordQualityEstimationDisabled = false
|
||||
override fun operate(node: EntryKDBX): Boolean {
|
||||
if (!node.qualityCheck) {
|
||||
passwordQualityEstimationDisabled = true
|
||||
}
|
||||
return super.operate(node)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
|
||||
var containsTags = false
|
||||
override fun operate(node: GroupKDBX): Boolean {
|
||||
if (!node.tags.isEmpty())
|
||||
containsTags = true
|
||||
return super.operate(node)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMinKdbxVersion(databaseKDBX: DatabaseKDBX): UnsignedInt {
|
||||
val entryHandler = EntryOperationHandler()
|
||||
val groupHandler = GroupOperationHandler()
|
||||
databaseKDBX.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
|
||||
|
||||
// https://keepass.info/help/kb/kdbx_4.1.html
|
||||
val containsGroupWithTag = groupHandler.containsTags
|
||||
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
|
||||
val containsCustomIconWithNameOrLastModificationTime = databaseKDBX.iconsManager.containsCustomIconWithNameOrLastModificationTime()
|
||||
val containsHeaderCustomDataWithLastModificationTime = databaseKDBX.customData.containsItemWithLastModificationTime()
|
||||
|
||||
// https://keepass.info/help/kb/kdbx_4.html
|
||||
// If AES is not use, it's at least 4.0
|
||||
val kdfIsNotAes = databaseKDBX.kdfParameters?.uuid != AesKdf.CIPHER_UUID
|
||||
val containsHeaderCustomData = databaseKDBX.customData.isNotEmpty()
|
||||
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
|
||||
|
||||
// Check each condition to determine version
|
||||
return if (containsGroupWithTag
|
||||
|| containsEntryWithPasswordQualityEstimationDisabled
|
||||
|| containsCustomIconWithNameOrLastModificationTime
|
||||
|| containsHeaderCustomDataWithLastModificationTime) {
|
||||
FILE_VERSION_41
|
||||
} else if (kdfIsNotAes
|
||||
|| containsHeaderCustomData
|
||||
|| containsNodeCustomData) {
|
||||
FILE_VERSION_40
|
||||
} else {
|
||||
FILE_VERSION_31
|
||||
}
|
||||
}
|
||||
|
||||
/** Assumes the input stream is at the beginning of the .kdbx file
|
||||
* @param inputStream
|
||||
* @throws IOException
|
||||
@@ -256,8 +191,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
if (pbId == null || pbId.size != 16) {
|
||||
throw IOException("Invalid cipher ID.")
|
||||
}
|
||||
|
||||
databaseV4.cipherUuid = bytes16ToUuid(pbId)
|
||||
databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
|
||||
}
|
||||
|
||||
private fun setTransformRound(roundsByte: ByteArray) {
|
||||
@@ -311,8 +245,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
|
||||
companion object {
|
||||
|
||||
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a)
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab40499)
|
||||
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
|
||||
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) // 0xB54BFB66
|
||||
val DBSIG_2 = UnsignedInt(-0x4ab40499) // 0xB54BFB67
|
||||
|
||||
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
|
||||
val FILE_VERSION_31 = UnsignedInt(0x00030001)
|
||||
@@ -335,7 +270,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
}
|
||||
|
||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||
return sig1 == DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,12 @@ package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
||||
(protected val cacheDirectory: File,
|
||||
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
|
||||
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var mDatabase: D) {
|
||||
|
||||
private var startTimeKey = System.currentTimeMillis()
|
||||
private var startTimeContent = System.currentTimeMillis()
|
||||
@@ -47,19 +43,8 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean = false): D
|
||||
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean = false): D
|
||||
assignMasterKey: (() -> Unit)): D
|
||||
|
||||
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||
|
||||
@@ -20,17 +20,16 @@
|
||||
|
||||
package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.graphics.Color
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
@@ -40,48 +39,18 @@ import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
|
||||
/**
|
||||
* Load a KDB database file.
|
||||
*/
|
||||
class DatabaseInputKDB(cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
||||
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
|
||||
|
||||
private lateinit var mDatabase: DatabaseKDB
|
||||
class DatabaseInputKDB(database: DatabaseKDB)
|
||||
: DatabaseInput<DatabaseKDB>(database) {
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
mDatabase.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean,
|
||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDB {
|
||||
|
||||
try {
|
||||
startKeyTimer(progressTaskUpdater)
|
||||
@@ -98,7 +67,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
|
||||
throw IOException("Header corrupted")
|
||||
|
||||
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1
|
||||
if (header.signature1 != DatabaseHeaderKDB.DBSIG_1
|
||||
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
|
||||
throw SignatureDatabaseException()
|
||||
}
|
||||
@@ -107,11 +76,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
throw VersionDatabaseException()
|
||||
}
|
||||
|
||||
mDatabase = DatabaseKDB()
|
||||
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
||||
|
||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||
assignMasterKey?.invoke()
|
||||
assignMasterKey.invoke()
|
||||
|
||||
// Select algorithm
|
||||
when {
|
||||
@@ -153,10 +118,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
)
|
||||
)
|
||||
|
||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||
val newRoot = mDatabase.createGroup()
|
||||
mDatabase.rootGroup = newRoot
|
||||
|
||||
// Import all nodes
|
||||
val groupLevelList = HashMap<GroupKDB, Int>()
|
||||
var newGroup: GroupKDB? = null
|
||||
@@ -285,7 +246,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
0x000E -> {
|
||||
newEntry?.let { entry ->
|
||||
if (fieldSize > 0) {
|
||||
val binaryData = mDatabase.buildNewAttachment()
|
||||
val binaryData = mDatabase.buildNewBinaryAttachment()
|
||||
entry.putBinary(binaryData, mDatabase.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
|
||||
cipherInputStream.readBytes(fieldSize) { buffer ->
|
||||
@@ -303,7 +264,34 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
newGroup = null
|
||||
}
|
||||
newEntry?.let { entry ->
|
||||
// Parse meta info
|
||||
when {
|
||||
entry.isMetaStreamDefaultUsername() -> {
|
||||
var defaultUser = ""
|
||||
entry.getBinary(mDatabase.attachmentPool)
|
||||
?.getInputDataStream(mDatabase.binaryCache)?.use {
|
||||
defaultUser = String(it.readBytes())
|
||||
}
|
||||
mDatabase.defaultUserName = defaultUser
|
||||
}
|
||||
entry.isMetaStreamDatabaseColor() -> {
|
||||
var color: Int? = null
|
||||
entry.getBinary(mDatabase.attachmentPool)
|
||||
?.getInputDataStream(mDatabase.binaryCache)?.use {
|
||||
val reverseColor = UnsignedInt(it.readBytes4ToUInt()).toKotlinInt()
|
||||
color = Color.rgb(
|
||||
Color.blue(reverseColor),
|
||||
Color.green(reverseColor),
|
||||
Color.red(reverseColor)
|
||||
)
|
||||
}
|
||||
mDatabase.color = color
|
||||
}
|
||||
// TODO manager other meta stream
|
||||
else -> {
|
||||
mDatabase.addEntryIndex(entry)
|
||||
}
|
||||
}
|
||||
currentEntryNumber++
|
||||
newEntry = null
|
||||
}
|
||||
@@ -323,16 +311,16 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
stopContentTimer()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw LoadDatabaseException(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,17 +24,16 @@ import android.util.Log
|
||||
import com.kunzisoft.encrypt.StreamCipher
|
||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
@@ -50,7 +49,6 @@ import com.kunzisoft.keepass.utils.*
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
@@ -63,12 +61,10 @@ import javax.crypto.CipherInputStream
|
||||
import javax.crypto.Mac
|
||||
import kotlin.math.min
|
||||
|
||||
class DatabaseInputKDBX(cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
||||
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
|
||||
class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
: DatabaseInput<DatabaseKDBX>(database) {
|
||||
|
||||
private var randomStream: StreamCipher? = null
|
||||
private lateinit var mDatabase: DatabaseKDBX
|
||||
|
||||
private var hashOfHeader: ByteArray? = null
|
||||
|
||||
@@ -97,42 +93,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
private var entryCustomDataKey: String? = null
|
||||
private var entryCustomDataValue: String? = null
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||
}
|
||||
private var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
|
||||
|
||||
fun setMethodToCheckIfRAMIsSufficient(method: (memoryWanted: Long) -> Boolean) {
|
||||
this.isRAMSufficient = method
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
mDatabase.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean,
|
||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDBX {
|
||||
try {
|
||||
startKeyTimer(progressTaskUpdater)
|
||||
mDatabase = DatabaseKDBX()
|
||||
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
||||
|
||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||
|
||||
val header = DatabaseHeaderKDBX(mDatabase)
|
||||
|
||||
@@ -142,19 +114,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
hashOfHeader = headerAndHash.hash
|
||||
val pbHeader = headerAndHash.header
|
||||
|
||||
assignMasterKey?.invoke()
|
||||
assignMasterKey.invoke()
|
||||
mDatabase.makeFinalKey(header.masterSeed)
|
||||
|
||||
stopKeyTimer()
|
||||
startContentTimer(progressTaskUpdater)
|
||||
|
||||
val engine: CipherEngine
|
||||
val cipher: Cipher
|
||||
try {
|
||||
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
|
||||
val engine: CipherEngine = mDatabase.encryptionAlgorithm.cipherEngine
|
||||
engine.forcePaddingCompatibility = true
|
||||
mDatabase.setDataEngine(engine)
|
||||
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
|
||||
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
||||
engine.forcePaddingCompatibility = false
|
||||
} catch (e: Exception) {
|
||||
@@ -288,7 +257,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||
val byteLength = size - 1
|
||||
// No compression at this level
|
||||
val protectedBinary = mDatabase.buildNewAttachment(
|
||||
val protectedBinary = mDatabase.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
|
||||
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||
dataInputStream.readBytes(byteLength) { buffer ->
|
||||
@@ -524,7 +493,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
||||
val iconUUID = readUuid(xpp)
|
||||
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
|
||||
ctxGroup?.tags = readTags(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
|
||||
@@ -583,7 +553,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
||||
val iconUUID = readUuid(xpp)
|
||||
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
||||
ctxEntry?.foregroundColor = readString(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
|
||||
@@ -704,7 +675,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||
ctxDeletedObject?.uuid = readUuid(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
||||
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp))
|
||||
ctxDeletedObject?.deletionTime = readDateInstant(xpp)
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
@@ -885,7 +856,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readTags(xpp: XmlPullParser): Tags {
|
||||
return Tags(readString(xpp))
|
||||
val tags = Tags(readString(xpp))
|
||||
mDatabase.tagPool.put(tags)
|
||||
return tags
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
@@ -1009,7 +982,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
var binaryRetrieve = mDatabase.attachmentPool[id]
|
||||
// Create empty binary if not retrieved in pool
|
||||
if (binaryRetrieve == null) {
|
||||
binaryRetrieve = mDatabase.buildNewAttachment(
|
||||
binaryRetrieve = mDatabase.buildNewBinaryAttachment(
|
||||
smallSize = false,
|
||||
compression = false,
|
||||
protection = false,
|
||||
@@ -1049,7 +1022,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
return null
|
||||
|
||||
// Build the new binary and compress
|
||||
val binaryAttachment = mDatabase.buildNewAttachment(
|
||||
val binaryAttachment = mDatabase.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
|
||||
try {
|
||||
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.stream.MacOutputStream
|
||||
@@ -68,11 +67,11 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
@Throws(IOException::class)
|
||||
fun output() {
|
||||
|
||||
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1)
|
||||
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_1)
|
||||
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
|
||||
mos.write4BytesUInt(header.version)
|
||||
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.cipherUuid))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.encryptionAlgorithm.uuid))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file.output
|
||||
|
||||
import android.graphics.Color
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
@@ -34,7 +36,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.*
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
|
||||
@@ -44,6 +45,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
|
||||
private var headerHashBlock: ByteArray? = null
|
||||
|
||||
private var mGroupList = mutableListOf<GroupKDB>()
|
||||
private var mEntryList = mutableListOf<EntryKDB>()
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun getFinalKey(header: DatabaseHeader): ByteArray? {
|
||||
try {
|
||||
@@ -61,7 +65,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
||||
// also remove the virtual root not present in kdb
|
||||
val rootGroup = mDatabaseKDB.rootGroup
|
||||
sortGroupsForOutput()
|
||||
sortNodesForOutput()
|
||||
|
||||
val header = outputHeader(mOutputStream)
|
||||
|
||||
@@ -91,6 +95,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
} finally {
|
||||
// Add again the virtual root group for better management
|
||||
mDatabaseKDB.rootGroup = rootGroup
|
||||
clearParser()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +110,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
|
||||
// Build header
|
||||
val header = DatabaseHeaderKDB()
|
||||
header.signature1 = DatabaseHeader.PWM_DBSIG_1
|
||||
header.signature1 = DatabaseHeaderKDB.DBSIG_1
|
||||
header.signature2 = DatabaseHeaderKDB.DBSIG_2
|
||||
header.flags = DatabaseHeaderKDB.FLAG_SHA2
|
||||
|
||||
@@ -120,8 +125,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
header.version = DatabaseHeaderKDB.DBVER_DW
|
||||
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
|
||||
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
|
||||
// To remove root
|
||||
header.numGroups = UnsignedInt(mGroupList.size)
|
||||
header.numEntries = UnsignedInt(mEntryList.size)
|
||||
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
|
||||
|
||||
setIVs(header)
|
||||
@@ -194,31 +200,89 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
// Groups
|
||||
mDatabaseKDB.doForEachGroupInIndex { group ->
|
||||
mGroupList.forEach { group ->
|
||||
if (group != mDatabaseKDB.rootGroup) {
|
||||
GroupOutputKDB(group, outputStream).output()
|
||||
}
|
||||
}
|
||||
// Entries
|
||||
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
||||
mEntryList.forEach { entry ->
|
||||
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortGroupsForOutput() {
|
||||
val groupList = ArrayList<GroupKDB>()
|
||||
// Rebuild list according to sorting order removing any orphaned groups
|
||||
for (rootGroup in mDatabaseKDB.rootGroups) {
|
||||
sortGroup(rootGroup, groupList)
|
||||
}
|
||||
mDatabaseKDB.setGroupIndexes(groupList)
|
||||
private fun clearParser() {
|
||||
mGroupList.clear()
|
||||
mEntryList.clear()
|
||||
}
|
||||
|
||||
private fun sortGroup(group: GroupKDB, groupList: MutableList<GroupKDB>) {
|
||||
private fun sortNodesForOutput() {
|
||||
clearParser()
|
||||
// Rebuild list according to sorting order removing any orphaned groups
|
||||
// Do not keep root
|
||||
mDatabaseKDB.rootGroup?.getChildGroups()?.let { rootSubGroups ->
|
||||
for (rootGroup in rootSubGroups) {
|
||||
sortGroup(rootGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortGroup(group: GroupKDB) {
|
||||
// Add current tree
|
||||
groupList.add(group)
|
||||
mGroupList.add(group)
|
||||
|
||||
for (childEntry in group.getChildEntries()) {
|
||||
if (!childEntry.isMetaStreamDefaultUsername()
|
||||
&& !childEntry.isMetaStreamDatabaseColor()) {
|
||||
mEntryList.add(childEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// Add MetaStream
|
||||
if (mDatabaseKDB.defaultUserName.isNotEmpty()) {
|
||||
val metaEntry = EntryKDB().apply {
|
||||
setMetaStreamDefaultUsername()
|
||||
setDefaultUsername(this)
|
||||
}
|
||||
mDatabaseKDB.addEntryTo(metaEntry, group)
|
||||
mEntryList.add(metaEntry)
|
||||
}
|
||||
if (mDatabaseKDB.color != null) {
|
||||
val metaEntry = EntryKDB().apply {
|
||||
setMetaStreamDatabaseColor()
|
||||
setDatabaseColor(this)
|
||||
}
|
||||
mDatabaseKDB.addEntryTo(metaEntry, group)
|
||||
mEntryList.add(metaEntry)
|
||||
}
|
||||
|
||||
// Recurse over children
|
||||
for (childGroup in group.getChildGroups()) {
|
||||
sortGroup(childGroup, groupList)
|
||||
sortGroup(childGroup)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultUsername(entryKDB: EntryKDB) {
|
||||
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
|
||||
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDatabaseColor(entryKDB: EntryKDB) {
|
||||
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
|
||||
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||
var reversColor = Color.BLACK
|
||||
mDatabaseKDB.color?.let {
|
||||
reversColor = Color.rgb(
|
||||
Color.blue(it),
|
||||
Color.green(it),
|
||||
Color.red(it)
|
||||
)
|
||||
}
|
||||
outputStream.write4BytesUInt(UnsignedInt(reversColor))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,7 @@ import android.util.Log
|
||||
import android.util.Xml
|
||||
import com.kunzisoft.encrypt.StreamCipher
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
@@ -39,7 +37,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
@@ -48,11 +45,9 @@ import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import org.joda.time.DateTime
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPOutputStream
|
||||
@@ -70,18 +65,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private var header: DatabaseHeaderKDBX? = null
|
||||
private var hashOfHeader: ByteArray? = null
|
||||
private var headerHmac: ByteArray? = null
|
||||
private var engine: CipherEngine? = null
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun output() {
|
||||
|
||||
try {
|
||||
try {
|
||||
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw DatabaseOutputException("No such cipher", e)
|
||||
}
|
||||
|
||||
header = outputHeader(mOutputStream)
|
||||
|
||||
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
@@ -241,6 +229,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
|
||||
}
|
||||
|
||||
if (!header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
|
||||
}
|
||||
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
|
||||
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
|
||||
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
|
||||
@@ -280,7 +271,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
|
||||
val cipher: Cipher
|
||||
try {
|
||||
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
|
||||
cipher = mDatabaseKDBX
|
||||
.encryptionAlgorithm
|
||||
.cipherEngine
|
||||
.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
|
||||
} catch (e: Exception) {
|
||||
throw DatabaseOutputException("Invalid algorithm.", e)
|
||||
}
|
||||
@@ -293,7 +287,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
val random = super.setIVs(header)
|
||||
random.nextBytes(header.masterSeed)
|
||||
|
||||
val ivLength = engine!!.ivLength()
|
||||
val ivLength = mDatabaseKDBX.encryptionAlgorithm.cipherEngine.ivLength()
|
||||
if (ivLength != header.encryptionIV.size) {
|
||||
header.encryptionIV = ByteArray(ivLength)
|
||||
}
|
||||
@@ -303,12 +297,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters
|
||||
}
|
||||
|
||||
try {
|
||||
val kdf = mDatabaseKDBX.getEngineKDBX4(mDatabaseKDBX.kdfParameters)
|
||||
kdf.randomize(mDatabaseKDBX.kdfParameters!!)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.e(TAG, "Unable to retrieve header", unknownKDF)
|
||||
}
|
||||
mDatabaseKDBX.randomizeKdfParameters()
|
||||
|
||||
if (header.version.isBefore(FILE_VERSION_40)) {
|
||||
header.innerRandomStream = CrsAlgorithm.Salsa20
|
||||
@@ -592,7 +581,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||
|
||||
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
||||
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
|
||||
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
|
||||
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||
}
|
||||
@@ -618,7 +607,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeDeletedObjects(value: List<DeletedObject>) {
|
||||
private fun writeDeletedObjects(value: Collection<DeletedObject>) {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
|
||||
|
||||
for (pdo in value) {
|
||||
@@ -674,7 +663,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeTags(tags: Tags) {
|
||||
if (!tags.isEmpty()) {
|
||||
if (tags.isNotEmpty()) {
|
||||
writeString(DatabaseKDBXXML.ElemTags, tags.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.merge
|
||||
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.CustomData
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.utils.readAllBytes
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
|
||||
var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
|
||||
|
||||
/**
|
||||
* Merge a KDB database in a KDBX database, by default all data are copied from the KDB
|
||||
*/
|
||||
fun merge(databaseToMerge: DatabaseKDB) {
|
||||
val rootGroup = database.rootGroup
|
||||
val rootGroupId = rootGroup?.nodeId
|
||||
val rootGroupToMerge = databaseToMerge.rootGroup
|
||||
val rootGroupIdToMerge = rootGroupToMerge?.nodeId
|
||||
|
||||
if (rootGroupId == null || rootGroupIdToMerge == null) {
|
||||
throw IOException("Database is not open")
|
||||
}
|
||||
|
||||
// Replace the UUID of the KDB root group to init seed
|
||||
databaseToMerge.removeGroupIndex(rootGroupToMerge)
|
||||
rootGroupToMerge.nodeId = NodeIdInt(0)
|
||||
databaseToMerge.addGroupIndex(rootGroupToMerge)
|
||||
|
||||
// Merge children
|
||||
rootGroupToMerge.doForEachChild(
|
||||
object : NodeHandler<EntryKDB>() {
|
||||
override fun operate(node: EntryKDB): Boolean {
|
||||
mergeEntry(rootGroup.nodeId, node, databaseToMerge)
|
||||
return true
|
||||
}
|
||||
},
|
||||
object : NodeHandler<GroupKDB>() {
|
||||
override fun operate(node: GroupKDB): Boolean {
|
||||
mergeGroup(rootGroup.nodeId, node, databaseToMerge)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to transform KDB id nodes in KDBX id nodes
|
||||
*/
|
||||
private fun getNodeIdUUIDFrom(seed: NodeId<UUID>, intId: NodeId<Int>): NodeId<UUID> {
|
||||
val seedUUID = seed.id
|
||||
val idInt = intId.id
|
||||
return NodeIdUUID(UUID(seedUUID.mostSignificantBits, seedUUID.leastSignificantBits + idInt))
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to merge a KDB entry
|
||||
*/
|
||||
private fun mergeEntry(seed: NodeId<UUID>, nodeToMerge: EntryKDB, databaseToMerge: DatabaseKDB) {
|
||||
val entryId: NodeId<UUID> = nodeToMerge.nodeId
|
||||
val entry = database.getEntryById(entryId)
|
||||
|
||||
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
|
||||
// Do not merge meta stream elements
|
||||
if (!srcEntryToMerge.isMetaStream()) {
|
||||
// Retrieve parent in current database
|
||||
var parentEntryToMerge: GroupKDBX? = null
|
||||
srcEntryToMerge.parent?.nodeId?.let {
|
||||
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
|
||||
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
|
||||
}
|
||||
// Copy attachment
|
||||
var newAttachment: Attachment? = null
|
||||
srcEntryToMerge.getAttachment(databaseToMerge.attachmentPool)?.let { attachment ->
|
||||
val binarySize = attachment.binaryData.getSize()
|
||||
val binaryData = database.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(binarySize),
|
||||
attachment.binaryData.isCompressed,
|
||||
attachment.binaryData.isProtected
|
||||
)
|
||||
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache)
|
||||
.use { inputStream ->
|
||||
binaryData.getOutputDataStream(database.binaryCache)
|
||||
.use { outputStream ->
|
||||
inputStream.readAllBytes { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
newAttachment = Attachment(attachment.name, binaryData)
|
||||
}
|
||||
// Create new entry format
|
||||
val entryToMerge = EntryKDBX().apply {
|
||||
this.nodeId = srcEntryToMerge.nodeId
|
||||
this.icon = srcEntryToMerge.icon
|
||||
this.creationTime = DateInstant(srcEntryToMerge.creationTime)
|
||||
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
|
||||
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
|
||||
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
|
||||
this.expires = srcEntryToMerge.expires
|
||||
this.title = srcEntryToMerge.title
|
||||
this.username = srcEntryToMerge.username
|
||||
this.password = srcEntryToMerge.password
|
||||
this.url = srcEntryToMerge.url
|
||||
this.notes = srcEntryToMerge.notes
|
||||
newAttachment?.let {
|
||||
this.putAttachment(it, database.attachmentPool)
|
||||
}
|
||||
}
|
||||
if (entry != null) {
|
||||
entry.updateWith(entryToMerge, false)
|
||||
} else if (parentEntryToMerge != null) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to merge a KDB group
|
||||
*/
|
||||
private fun mergeGroup(seed: NodeId<UUID>, nodeToMerge: GroupKDB, databaseToMerge: DatabaseKDB) {
|
||||
val groupId: NodeId<Int> = nodeToMerge.nodeId
|
||||
val group = database.getGroupById(getNodeIdUUIDFrom(seed, groupId))
|
||||
|
||||
databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
|
||||
// Retrieve parent in current database
|
||||
var parentGroupToMerge: GroupKDBX? = null
|
||||
srcGroupToMerge.parent?.nodeId?.let {
|
||||
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
|
||||
parentGroupToMerge = database.getGroupById(parentGroupIdToMerge)
|
||||
}
|
||||
val groupToMerge = GroupKDBX().apply {
|
||||
this.nodeId = getNodeIdUUIDFrom(seed, srcGroupToMerge.nodeId)
|
||||
this.icon = srcGroupToMerge.icon
|
||||
this.creationTime = DateInstant(srcGroupToMerge.creationTime)
|
||||
this.lastModificationTime = DateInstant(srcGroupToMerge.lastModificationTime)
|
||||
this.lastAccessTime = DateInstant(srcGroupToMerge.lastAccessTime)
|
||||
this.expiryTime = DateInstant(srcGroupToMerge.expiryTime)
|
||||
this.expires = srcGroupToMerge.expires
|
||||
this.title = srcGroupToMerge.title
|
||||
}
|
||||
if (group != null) {
|
||||
group.updateWith(groupToMerge, false)
|
||||
} else if (parentGroupToMerge != null) {
|
||||
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a KDB> database in a KDBX database,
|
||||
* Try to take into account the modification date of each element
|
||||
* To make a merge as accurate as possible
|
||||
*/
|
||||
fun merge(databaseToMerge: DatabaseKDBX) {
|
||||
|
||||
// Merge settings
|
||||
if (database.nameChanged.date.before(databaseToMerge.nameChanged.date)) {
|
||||
database.name = databaseToMerge.name
|
||||
database.nameChanged = databaseToMerge.nameChanged
|
||||
}
|
||||
if (database.descriptionChanged.date.before(databaseToMerge.descriptionChanged.date)) {
|
||||
database.description = databaseToMerge.description
|
||||
database.descriptionChanged = databaseToMerge.descriptionChanged
|
||||
}
|
||||
if (database.defaultUserNameChanged.date.before(databaseToMerge.defaultUserNameChanged.date)) {
|
||||
database.defaultUserName = databaseToMerge.defaultUserName
|
||||
database.defaultUserNameChanged = databaseToMerge.defaultUserNameChanged
|
||||
}
|
||||
if (database.keyLastChanged.date.before(databaseToMerge.keyLastChanged.date)) {
|
||||
database.keyChangeRecDays = databaseToMerge.keyChangeRecDays
|
||||
database.keyChangeForceDays = databaseToMerge.keyChangeForceDays
|
||||
database.isKeyChangeForceOnce = databaseToMerge.isKeyChangeForceOnce
|
||||
database.keyLastChanged = databaseToMerge.keyLastChanged
|
||||
}
|
||||
if (database.recycleBinChanged.date.before(databaseToMerge.recycleBinChanged.date)) {
|
||||
database.isRecycleBinEnabled = databaseToMerge.isRecycleBinEnabled
|
||||
database.recycleBinUUID = databaseToMerge.recycleBinUUID
|
||||
database.recycleBinChanged = databaseToMerge.recycleBinChanged
|
||||
}
|
||||
if (database.entryTemplatesGroupChanged.date.before(databaseToMerge.entryTemplatesGroupChanged.date)) {
|
||||
database.entryTemplatesGroup = databaseToMerge.entryTemplatesGroup
|
||||
database.entryTemplatesGroupChanged = databaseToMerge.entryTemplatesGroupChanged
|
||||
}
|
||||
if (database.settingsChanged.date.before(databaseToMerge.settingsChanged.date)) {
|
||||
database.color = databaseToMerge.color
|
||||
database.compressionAlgorithm = databaseToMerge.compressionAlgorithm
|
||||
database.historyMaxItems = databaseToMerge.historyMaxItems
|
||||
database.historyMaxSize = databaseToMerge.historyMaxSize
|
||||
database.encryptionAlgorithm = databaseToMerge.encryptionAlgorithm
|
||||
database.kdfEngine = databaseToMerge.kdfEngine
|
||||
database.numberKeyEncryptionRounds = databaseToMerge.numberKeyEncryptionRounds
|
||||
database.memoryUsage = databaseToMerge.memoryUsage
|
||||
database.parallelism = databaseToMerge.parallelism
|
||||
database.settingsChanged = databaseToMerge.settingsChanged
|
||||
}
|
||||
|
||||
val rootGroup = database.rootGroup
|
||||
val rootGroupId = rootGroup?.nodeId
|
||||
val rootGroupToMerge = databaseToMerge.rootGroup
|
||||
val rootGroupIdToMerge = rootGroupToMerge?.nodeId
|
||||
|
||||
if (rootGroupId == null || rootGroupIdToMerge == null) {
|
||||
throw IOException("Database is not open")
|
||||
}
|
||||
|
||||
// UUID of the root group to merge is unknown
|
||||
if (database.getGroupById(rootGroupIdToMerge) == null) {
|
||||
// Change it to copy children database root
|
||||
databaseToMerge.removeGroupIndex(rootGroupToMerge)
|
||||
rootGroupToMerge.nodeId = rootGroupId
|
||||
databaseToMerge.addGroupIndex(rootGroupToMerge)
|
||||
}
|
||||
|
||||
// Merge root group
|
||||
if (rootGroup.lastModificationTime.date
|
||||
.before(rootGroupToMerge.lastModificationTime.date)) {
|
||||
rootGroup.updateWith(rootGroupToMerge, updateParents = false)
|
||||
}
|
||||
// Merge children
|
||||
rootGroupToMerge.doForEachChild(
|
||||
object : NodeHandler<EntryKDBX>() {
|
||||
override fun operate(node: EntryKDBX): Boolean {
|
||||
mergeEntry(node, databaseToMerge)
|
||||
return true
|
||||
}
|
||||
},
|
||||
object : NodeHandler<GroupKDBX>() {
|
||||
override fun operate(node: GroupKDBX): Boolean {
|
||||
mergeGroup(node, databaseToMerge)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Merge custom data in database header
|
||||
mergeCustomData(database.customData, databaseToMerge.customData)
|
||||
|
||||
// Merge icons
|
||||
databaseToMerge.iconsManager.doForEachCustomIcon { iconImageCustom, binaryData ->
|
||||
val customIconUuid = iconImageCustom.uuid
|
||||
// If custom icon not present, add it
|
||||
val customIcon = database.iconsManager.getIcon(customIconUuid)
|
||||
if (customIcon == null) {
|
||||
database.addCustomIcon(
|
||||
customIconUuid,
|
||||
iconImageCustom.name,
|
||||
iconImageCustom.lastModificationTime,
|
||||
false
|
||||
) { _, newBinaryData ->
|
||||
binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
|
||||
newBinaryData?.getOutputDataStream(database.binaryCache).use { outputStream ->
|
||||
inputStream.readAllBytes { buffer ->
|
||||
outputStream?.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val customIconModification = customIcon.lastModificationTime
|
||||
val customIconToMerge = databaseToMerge.iconsManager.getIcon(customIconUuid)
|
||||
val customIconModificationToMerge = customIconToMerge?.lastModificationTime
|
||||
if (customIconModification != null && customIconModificationToMerge != null) {
|
||||
if (customIconModification.date.before(customIconModificationToMerge.date)) {
|
||||
customIcon.updateWith(customIconToMerge)
|
||||
}
|
||||
} else if (customIconModificationToMerge != null) {
|
||||
customIcon.updateWith(customIconToMerge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manage deleted objects
|
||||
databaseToMerge.deletedObjects.forEach { deletedObject ->
|
||||
val deletedObjectId = deletedObject.uuid
|
||||
val databaseEntry = database.getEntryById(deletedObjectId)
|
||||
val databaseGroup = database.getGroupById(deletedObjectId)
|
||||
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
|
||||
val databaseIconModificationTime = databaseIcon?.lastModificationTime
|
||||
if (databaseEntry != null
|
||||
&& deletedObject.deletionTime.date
|
||||
.after(databaseEntry.lastModificationTime.date)) {
|
||||
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
|
||||
}
|
||||
if (databaseGroup != null
|
||||
&& deletedObject.deletionTime.date
|
||||
.after(databaseGroup.lastModificationTime.date)) {
|
||||
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
|
||||
}
|
||||
if (databaseIcon != null
|
||||
&& (
|
||||
databaseIconModificationTime == null
|
||||
|| (deletedObject.deletionTime.date.after(databaseIconModificationTime.date))
|
||||
)
|
||||
) {
|
||||
database.removeCustomIcon(deletedObjectId)
|
||||
}
|
||||
// Attachments are removed and optimized during the database save
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge [customDataToMerge] in [customData]
|
||||
*/
|
||||
private fun mergeCustomData(customData: CustomData, customDataToMerge: CustomData) {
|
||||
customDataToMerge.doForEachItems { customDataItemToMerge ->
|
||||
val customDataItem = customData.get(customDataItemToMerge.key)
|
||||
if (customDataItem == null) {
|
||||
customData.put(customDataItemToMerge)
|
||||
} else {
|
||||
val customDataItemModification = customDataItem.lastModificationTime
|
||||
val customDataItemToMergeModification = customDataItemToMerge.lastModificationTime
|
||||
if (customDataItemModification != null && customDataItemToMergeModification != null) {
|
||||
if (customDataItemModification.date
|
||||
.before(customDataItemToMergeModification.date)) {
|
||||
customData.put(customDataItemToMerge)
|
||||
}
|
||||
} else {
|
||||
customData.put(customDataItemToMerge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to merge a KDBX entry
|
||||
*/
|
||||
private fun mergeEntry(nodeToMerge: EntryKDBX, databaseToMerge: DatabaseKDBX) {
|
||||
val entryId = nodeToMerge.nodeId
|
||||
val entry = database.getEntryById(entryId)
|
||||
val deletedObject = database.getDeletedObject(entryId)
|
||||
|
||||
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
|
||||
// Retrieve parent in current database
|
||||
var parentEntryToMerge: GroupKDBX? = null
|
||||
srcEntryToMerge.parent?.nodeId?.let {
|
||||
parentEntryToMerge = database.getGroupById(it)
|
||||
}
|
||||
val entryToMerge = EntryKDBX().apply {
|
||||
updateWith(srcEntryToMerge, copyHistory = true, updateParents = false)
|
||||
}
|
||||
|
||||
// Copy attachments in main pool
|
||||
val newAttachments = mutableListOf<Attachment>()
|
||||
entryToMerge.getAttachments(databaseToMerge.attachmentPool).forEach { attachment ->
|
||||
val binarySize = attachment.binaryData.getSize()
|
||||
val binaryData = database.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(binarySize),
|
||||
attachment.binaryData.isCompressed,
|
||||
attachment.binaryData.isProtected
|
||||
)
|
||||
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
|
||||
binaryData.getOutputDataStream(database.binaryCache).use { outputStream ->
|
||||
inputStream.readAllBytes { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
newAttachments.add(Attachment(attachment.name, binaryData))
|
||||
}
|
||||
entryToMerge.removeAttachments()
|
||||
newAttachments.forEach { newAttachment ->
|
||||
entryToMerge.putAttachment(newAttachment, database.attachmentPool)
|
||||
}
|
||||
|
||||
if (entry == null) {
|
||||
// If it's a deleted object, but another instance was updated
|
||||
// If entry parent to add exists and in current database
|
||||
if ((deletedObject == null
|
||||
|| deletedObject.deletionTime.date
|
||||
.before(entryToMerge.lastModificationTime.date))
|
||||
&& parentEntryToMerge != null) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
}
|
||||
} else {
|
||||
// Merge independently custom data
|
||||
mergeCustomData(entry.customData, entryToMerge.customData)
|
||||
// Merge by modification time
|
||||
if (entry.lastModificationTime.date
|
||||
.before(entryToMerge.lastModificationTime.date)
|
||||
) {
|
||||
addHistory(entry, entryToMerge)
|
||||
if (parentEntryToMerge == entry.parent) {
|
||||
entry.updateWith(entryToMerge, copyHistory = true, updateParents = false)
|
||||
} else {
|
||||
// Update entry with databaseEntryToMerge and merge history
|
||||
database.removeEntryFrom(entry, entry.parent)
|
||||
if (parentEntryToMerge != null) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
}
|
||||
}
|
||||
} else if (entry.lastModificationTime.date
|
||||
.after(entryToMerge.lastModificationTime.date)
|
||||
) {
|
||||
addHistory(entryToMerge, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to merge an history from an [entryA] to an [entryB],
|
||||
* [entryB] is modified
|
||||
*/
|
||||
private fun addHistory(entryA: EntryKDBX, entryB: EntryKDBX) {
|
||||
// Keep entry as history if already not present
|
||||
entryA.history.forEach { history ->
|
||||
// If history not present
|
||||
if (!entryB.history.any {
|
||||
it.lastModificationTime == history.lastModificationTime
|
||||
}) {
|
||||
entryB.addEntryToHistory(history)
|
||||
}
|
||||
}
|
||||
// Last entry not present
|
||||
if (entryB.history.find {
|
||||
it.lastModificationTime == entryA.lastModificationTime
|
||||
} == null) {
|
||||
val history = EntryKDBX().apply {
|
||||
updateWith(entryA, copyHistory = false, updateParents = false)
|
||||
parent = null
|
||||
}
|
||||
entryB.addEntryToHistory(history)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to merge a KDBX group
|
||||
*/
|
||||
private fun mergeGroup(nodeToMerge: GroupKDBX, databaseToMerge: DatabaseKDBX) {
|
||||
val groupId = nodeToMerge.nodeId
|
||||
val group = database.getGroupById(groupId)
|
||||
val deletedObject = database.getDeletedObject(groupId)
|
||||
|
||||
databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
|
||||
// Retrieve parent in current database
|
||||
var parentGroupToMerge: GroupKDBX? = null
|
||||
srcGroupToMerge.parent?.nodeId?.let {
|
||||
parentGroupToMerge = database.getGroupById(it)
|
||||
}
|
||||
val groupToMerge = GroupKDBX().apply {
|
||||
updateWith(srcGroupToMerge, updateParents = false)
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
// If group parent to add exists and in current database
|
||||
if ((deletedObject == null
|
||||
|| deletedObject.deletionTime.date
|
||||
.before(groupToMerge.lastModificationTime.date))
|
||||
&& parentGroupToMerge != null) {
|
||||
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||
}
|
||||
} else {
|
||||
// Merge independently custom data
|
||||
mergeCustomData(group.customData, groupToMerge.customData)
|
||||
// Merge by modification time
|
||||
if (group.lastModificationTime.date
|
||||
.before(groupToMerge.lastModificationTime.date)
|
||||
) {
|
||||
if (parentGroupToMerge == group.parent) {
|
||||
group.updateWith(groupToMerge, false)
|
||||
} else {
|
||||
database.removeGroupFrom(group, group.parent)
|
||||
if (parentGroupToMerge != null) {
|
||||
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
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.NodeId
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||
@@ -37,16 +38,23 @@ class SearchHelper {
|
||||
|
||||
fun createVirtualGroupWithSearchResult(database: Database,
|
||||
searchParameters: SearchParameters,
|
||||
omitBackup: Boolean,
|
||||
fromGroup: NodeId<*>? = null,
|
||||
max: Int): Group? {
|
||||
|
||||
val searchGroup = database.createGroup()
|
||||
searchGroup?.isVirtual = true
|
||||
val searchGroup = database.createGroup(virtual = true)
|
||||
searchGroup?.title = "\"" + searchParameters.searchQuery + "\""
|
||||
|
||||
// Search all entries
|
||||
incrementEntry = 0
|
||||
database.rootGroup?.doForEachChild(
|
||||
|
||||
val allowCustomSearchable = database.allowCustomSearchableGroup()
|
||||
val startGroup = if (searchParameters.searchInCurrentGroup && fromGroup != null) {
|
||||
database.getGroupById(fromGroup) ?: database.rootGroup
|
||||
} else {
|
||||
database.rootGroup
|
||||
}
|
||||
if (groupConditions(database, startGroup, searchParameters, allowCustomSearchable, max)) {
|
||||
startGroup?.doForEachChild(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
if (incrementEntry >= max)
|
||||
@@ -63,19 +71,43 @@ class SearchHelper {
|
||||
},
|
||||
object : NodeHandler<Group>() {
|
||||
override fun operate(node: Group): Boolean {
|
||||
return when {
|
||||
incrementEntry >= max -> false
|
||||
database.isGroupSearchable(node, omitBackup) -> true
|
||||
else -> false
|
||||
}
|
||||
return groupConditions(database,
|
||||
node,
|
||||
searchParameters,
|
||||
allowCustomSearchable,
|
||||
max
|
||||
)
|
||||
}
|
||||
},
|
||||
false)
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
searchGroup?.refreshNumberOfChildEntries()
|
||||
return searchGroup
|
||||
}
|
||||
|
||||
private fun groupConditions(database: Database,
|
||||
group: Group?,
|
||||
searchParameters: SearchParameters,
|
||||
allowCustomSearchable: Boolean,
|
||||
max: Int): Boolean {
|
||||
return if (group == null)
|
||||
false
|
||||
else if (incrementEntry >= max)
|
||||
false
|
||||
else if (database.groupIsInRecycleBin(group))
|
||||
searchParameters.searchInRecycleBin
|
||||
else if (database.groupIsInTemplates(group))
|
||||
searchParameters.searchInTemplates
|
||||
else if (!allowCustomSearchable)
|
||||
true
|
||||
else if (searchParameters.searchInSearchableGroup)
|
||||
group.isSearchable()
|
||||
else
|
||||
true
|
||||
}
|
||||
|
||||
private fun entryContainsString(database: Database,
|
||||
entry: Entry,
|
||||
searchParameters: SearchParameters): Boolean {
|
||||
@@ -89,7 +121,18 @@ class SearchHelper {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_SEARCH_ENTRY = 10
|
||||
const val MAX_SEARCH_ENTRY = 1000
|
||||
|
||||
/**
|
||||
* Method to show the number of search results with max results
|
||||
*/
|
||||
fun showNumberOfSearchResults(number: Int): String {
|
||||
return if (number >= MAX_SEARCH_ENTRY) {
|
||||
(MAX_SEARCH_ENTRY-1).toString() + "+"
|
||||
} else {
|
||||
number.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||
@@ -111,7 +154,6 @@ class SearchHelper {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo.toString(),
|
||||
PreferencesUtil.omitBackup(context),
|
||||
MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
@@ -133,16 +175,23 @@ class SearchHelper {
|
||||
fun searchInEntry(entry: Entry,
|
||||
searchParameters: SearchParameters): Boolean {
|
||||
val searchQuery = searchParameters.searchQuery
|
||||
// Entry don't contains string if the search string is empty
|
||||
|
||||
// Not found if the search string is empty
|
||||
if (searchQuery.isEmpty())
|
||||
return false
|
||||
|
||||
// Exclude entry expired
|
||||
if (searchParameters.excludeExpired) {
|
||||
if (entry.isCurrentlyExpires)
|
||||
return false
|
||||
}
|
||||
|
||||
// Search all strings in the KDBX entry
|
||||
if (searchParameters.searchInTitles) {
|
||||
if (checkSearchQuery(entry.title, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUserNames) {
|
||||
if (searchParameters.searchInUsernames) {
|
||||
if (checkSearchQuery(entry.username, searchParameters))
|
||||
return true
|
||||
}
|
||||
@@ -159,8 +208,8 @@ class SearchHelper {
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUUIDs) {
|
||||
val hexString = UuidUtil.toHexString(entry.nodeId.id)
|
||||
if (hexString != null && hexString.contains(searchQuery, true))
|
||||
val hexString = UuidUtil.toHexString(entry.nodeId.id) ?: ""
|
||||
if (checkSearchQuery(hexString, searchParameters))
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInOther) {
|
||||
@@ -172,21 +221,31 @@ class SearchHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (searchParameters.searchInTags) {
|
||||
if (checkSearchQuery(entry.tags.toString(), searchParameters))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
|
||||
/*
|
||||
// TODO Search settings
|
||||
var regularExpression = false
|
||||
var ignoreCase = true
|
||||
var removeAccents = true <- Too much time, to study
|
||||
var excludeExpired = false
|
||||
var searchOnlyInCurrentGroup = false
|
||||
*/
|
||||
return stringToCheck.isNotEmpty()
|
||||
&& stringToCheck.contains(
|
||||
searchParameters.searchQuery, true)
|
||||
if (stringToCheck.isEmpty())
|
||||
return false
|
||||
return if (searchParameters.isRegex) {
|
||||
val regex = if (searchParameters.caseSensitive) {
|
||||
searchParameters.searchQuery.toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
} else {
|
||||
searchParameters.searchQuery
|
||||
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
|
||||
}
|
||||
regex.matches(stringToCheck)
|
||||
} else {
|
||||
stringToCheck.contains(searchParameters.searchQuery, !searchParameters.caseSensitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,21 +19,82 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.search
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
/**
|
||||
* Parameters for searching strings in the database.
|
||||
*/
|
||||
class SearchParameters {
|
||||
class SearchParameters() : Parcelable{
|
||||
var searchQuery: String = ""
|
||||
var caseSensitive = false
|
||||
var isRegex = false
|
||||
|
||||
var searchInTitles = true
|
||||
var searchInUserNames = true
|
||||
var searchInUsernames = true
|
||||
var searchInPasswords = false
|
||||
var searchInUrls = true
|
||||
var excludeExpired = false
|
||||
var searchInNotes = true
|
||||
var searchInOTP = false
|
||||
var searchInOther = true
|
||||
var searchInUUIDs = false
|
||||
var searchInTags = true
|
||||
var searchInTags = false
|
||||
|
||||
var searchInCurrentGroup = false
|
||||
var searchInSearchableGroup = true
|
||||
var searchInRecycleBin = false
|
||||
var searchInTemplates = false
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
searchQuery = parcel.readString() ?: searchQuery
|
||||
caseSensitive = parcel.readByte() != 0.toByte()
|
||||
searchInTitles = parcel.readByte() != 0.toByte()
|
||||
searchInUsernames = parcel.readByte() != 0.toByte()
|
||||
searchInPasswords = parcel.readByte() != 0.toByte()
|
||||
searchInUrls = parcel.readByte() != 0.toByte()
|
||||
excludeExpired = parcel.readByte() != 0.toByte()
|
||||
searchInNotes = parcel.readByte() != 0.toByte()
|
||||
searchInOTP = parcel.readByte() != 0.toByte()
|
||||
searchInOther = parcel.readByte() != 0.toByte()
|
||||
searchInUUIDs = parcel.readByte() != 0.toByte()
|
||||
searchInTags = parcel.readByte() != 0.toByte()
|
||||
searchInCurrentGroup = parcel.readByte() != 0.toByte()
|
||||
searchInSearchableGroup = parcel.readByte() != 0.toByte()
|
||||
searchInRecycleBin = parcel.readByte() != 0.toByte()
|
||||
searchInTemplates = parcel.readByte() != 0.toByte()
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(searchQuery)
|
||||
parcel.writeByte(if (caseSensitive) 1 else 0)
|
||||
parcel.writeByte(if (searchInTitles) 1 else 0)
|
||||
parcel.writeByte(if (searchInUsernames) 1 else 0)
|
||||
parcel.writeByte(if (searchInPasswords) 1 else 0)
|
||||
parcel.writeByte(if (searchInUrls) 1 else 0)
|
||||
parcel.writeByte(if (excludeExpired) 1 else 0)
|
||||
parcel.writeByte(if (searchInNotes) 1 else 0)
|
||||
parcel.writeByte(if (searchInOTP) 1 else 0)
|
||||
parcel.writeByte(if (searchInOther) 1 else 0)
|
||||
parcel.writeByte(if (searchInUUIDs) 1 else 0)
|
||||
parcel.writeByte(if (searchInTags) 1 else 0)
|
||||
parcel.writeByte(if (searchInCurrentGroup) 1 else 0)
|
||||
parcel.writeByte(if (searchInSearchableGroup) 1 else 0)
|
||||
parcel.writeByte(if (searchInRecycleBin) 1 else 0)
|
||||
parcel.writeByte(if (searchInTemplates) 1 else 0)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<SearchParameters> {
|
||||
override fun createFromParcel(parcel: Parcel): SearchParameters {
|
||||
return SearchParameters(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SearchParameters?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user