Compare commits

...

131 Commits
3.0.3 ... 3.1.0

Author SHA1 Message Date
J-Jamet
8413d2b31a Merge branch 'release/3.1' 2022-01-18 12:44:44 +01:00
J-Jamet
04a03da382 Merge branch 'master' into release/3.1 2022-01-18 12:44:30 +01:00
J-Jamet
e3274657ea /bin/bash: q: command not found 2022-01-18 12:13:03 +01:00
J-Jamet
f3b25cb792 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2022-01-18 12:08:48 +01:00
J-Jamet
d181f886fe Update CHANGELOG 2022-01-18 12:02:33 +01:00
J-Jamet
616d073395 Capture duplicate error exception 2022-01-18 11:44:45 +01:00
J-Jamet
d36fc19585 Add try catch exception when populate view 2022-01-18 11:32:11 +01:00
J-Jamet
95d9e07e2f TODO fix meta stream 2022-01-17 23:51:37 +01:00
solokot
91ebf2ba6f Translated using Weblate (Russian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-15 11:13:08 +01:00
J-Jamet
9e97042dd1 Larger database color view 2022-01-14 18:42:32 +01:00
J-Jamet
b7c4a99e71 Upgrade README Year 2022-01-14 18:37:29 +01:00
J-Jamet
48e315453e Smooth counter 2022-01-14 18:11:36 +01:00
J-Jamet
a8a6d14ca3 Merge branch 'feature/Entry_Color' into develop 2022-01-14 18:05:21 +01:00
J-Jamet
e895dd3430 Upgrade CHANGELOG 2022-01-14 18:05:03 +01:00
J-Jamet
f59859137a Upgrade libs 2022-01-14 18:03:00 +01:00
J-Jamet
dee92e9e40 Upgrade libs 2022-01-14 18:01:35 +01:00
J-Jamet
6701f4f95e Fix color in KitKat 2022-01-14 17:55:54 +01:00
J-Jamet
e20f769854 Fix theme refresh 2022-01-14 16:00:36 +01:00
J-Jamet
4f762a9432 Change background color icon 2022-01-14 15:54:41 +01:00
J-Jamet
3c49eb1635 Fix color picker 2022-01-14 15:45:54 +01:00
J-Jamet
bdc6a282e2 Colorize entry view 2022-01-14 14:25:06 +01:00
J-Jamet
8392ab2cc4 Fix icon color 2022-01-14 01:13:37 +01:00
Allan Nordhøy
93c7c09f8c Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.0% (514 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2022-01-14 00:53:27 +01:00
Allan Nordhøy
120116414f Translated using Weblate (Danish)
Currently translated at 91.5% (523 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2022-01-14 00:53:24 +01:00
J-Jamet
11794e5819 Add color change listener 2022-01-14 00:39:20 +01:00
J-Jamet
edce3d7bec Add color in entry 2022-01-13 20:22:10 +01:00
J-Jamet
e133e32e7c Add color in app bar 2022-01-13 19:15:18 +01:00
J-Jamet
471859e448 Add color in entry edit 2022-01-13 18:08:20 +01:00
Óscar Fernández Díaz
d8de66eb14 Translated using Weblate (Spanish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-01-12 22:22:29 +01:00
J-Jamet
cfdc0237d7 Add foreground and background colors in list 2022-01-12 19:50:38 +01:00
J-Jamet
05fad24eda Fix color picker fragment 2022-01-12 17:01:35 +01:00
J-Jamet
d4818c5567 Select entry colors 2022-01-12 14:19:04 +01:00
J-Jamet
8e8e6a7b93 Invert info container visibility 2022-01-11 18:02:43 +01:00
J-Jamet
6547f0ffad First entry color test 2022-01-11 17:46:50 +01:00
Ngô Ngọc Đức Huy
6f172fffa8 Translated using Weblate (Vietnamese)
Currently translated at 29.5% (169 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2022-01-11 13:49:38 +01:00
Matthaiks
ed16e06676 Translated using Weblate (Polish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-11 13:49:38 +01:00
Ngô Ngọc Đức Huy
1874f06f42 Translated using Weblate (Vietnamese)
Currently translated at 29.4% (168 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2022-01-09 01:54:52 +01:00
zeritti
e9db24429a Translated using Weblate (Czech)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2022-01-09 01:54:51 +01:00
J-Jamet
a59f4d45ca Better color implementation 2022-01-08 22:29:43 +01:00
J-Jamet
c7b3e0926c Fix save database color 2022-01-08 21:45:11 +01:00
J-Jamet
f0f5258bc9 Fix refresh UI 2022-01-08 20:36:59 +01:00
J-Jamet
12c07cf793 Fix refresh database metadata 2022-01-08 20:22:39 +01:00
J-Jamet
d2b8c85015 Add database color #913 2022-01-08 20:03:56 +01:00
J-Jamet
b9652291bd Manage default user name and color in KDB 2022-01-08 19:35:42 +01:00
J-Jamet
b0d1f93bfc Change header sig 2022-01-08 16:06:10 +01:00
J-Jamet
553416c927 Fix number of entries and refactor GroupFragment 2022-01-07 18:59:23 +01:00
J-Jamet
b83696bc60 Fix create entry in the right group 2022-01-07 18:28:44 +01:00
J-Jamet
23ce320d75 Fix output KDB and private indexes 2022-01-07 18:12:45 +01:00
J-Jamet
27d5733dbc Strike out expires group 2022-01-07 14:38:18 +01:00
ssantos
8d7d01bf88 Translated using Weblate (Portuguese)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-01-03 21:55:51 +01:00
solokot
0bc37d2fc2 Translated using Weblate (Russian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-01-03 21:55:50 +01:00
André Marcelo Alvarenga
aaa1655af1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-03 21:55:50 +01:00
Mr-Update
f1bd4e1bba Translated using Weblate (German)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-01-03 21:55:50 +01:00
J-Jamet
15a28e7c83 Fix action in breadcrumb 2022-01-03 19:51:59 +01:00
Serdar Sağlam
ab26e561fd Translated using Weblate (Turkish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2022-01-01 13:56:33 +01:00
Eric
66968a28a3 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-01-01 13:56:32 +01:00
Ihor Hordiichuk
37d1f91224 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-01-01 13:56:32 +01:00
Gabriel Cardoso
9e69068d42 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2022-01-01 13:56:32 +01:00
Matthaiks
8e2a9fcd01 Translated using Weblate (Polish)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-01-01 13:56:31 +01:00
Retrial
1753887916 Translated using Weblate (Greek)
Currently translated at 100.0% (571 of 571 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-01-01 13:56:31 +01:00
Hosted Weblate
21bcffcc87 Merge branch 'origin/develop' into Weblate. 2021-12-31 17:27:28 +01:00
I. Musthafa
1caed49c75 Translated using Weblate (Indonesian)
Currently translated at 84.7% (482 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-31 09:20:33 +01:00
Vitor Henrique
619ea35168 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-12-31 09:20:32 +01:00
Giai Ngo
877f913e8f Translated using Weblate (Vietnamese)
Currently translated at 25.8% (147 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2021-12-23 03:50:28 +01:00
I. Musthafa
25c47390c0 Translated using Weblate (Indonesian)
Currently translated at 81.1% (462 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-23 03:50:26 +01:00
J-Jamet
b3c46348a1 Allow to change root group 2021-12-22 19:01:51 +01:00
J-Jamet
6f154194f1 Upgrade bouncy castle #833 2021-12-22 17:40:18 +01:00
J-Jamet
c3ab08ce17 Upgrade to SDK 31 2021-12-22 17:28:29 +01:00
J-Jamet
004fffa992 Add exact alarm message 2021-12-22 17:23:57 +01:00
J-Jamet
d6bd80c9c0 Fix UI in Android 8 #509 2021-12-22 14:12:30 +01:00
J-Jamet
318bcdd011 Remove WRITE_EXTERNAL_STORAGE permission 2021-12-22 13:57:05 +01:00
J-Jamet
3076f2af68 Add backup rules 2021-12-21 17:14:30 +01:00
Jérémy JAMET
3dd9ef5564 Update bug_report.md 2021-12-21 10:01:34 +01:00
I. Musthafa
367e5fa84e Translated using Weblate (Indonesian)
Currently translated at 81.0% (461 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-12-20 15:50:26 +01:00
J-Jamet
97cd61fd13 First pass to update API 31 2021-12-17 17:57:09 +01:00
Y. Sakamoto
869bf7a345 Translated using Weblate (Japanese)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-12-17 12:53:25 +01:00
J-Jamet
9e15ac242d Fix small element 2021-12-16 21:36:55 +01:00
J-Jamet
84943e58f1 Fix small elements 2021-12-16 21:30:40 +01:00
J-Jamet
2289bf0a27 Fix show UUID in V1 format 2021-12-16 21:15:23 +01:00
J-Jamet
7fda40c983 Show UUID in group 2021-12-16 20:52:49 +01:00
J-Jamet
7eeed8f670 Edit group on long click 2021-12-16 20:20:46 +01:00
J-Jamet
4d3f4ed5c2 Add group info dialog #1177 2021-12-16 19:12:31 +01:00
J-Jamet
145a4f5c20 Merge branch 'feature/Breadcrumb' into develop 2021-12-16 17:44:20 +01:00
J-Jamet
9afe3d26e9 Arrow as breadcrumb delimiter 2021-12-16 17:37:17 +01:00
J-Jamet
b73a7f1ed8 Upgrade version to 3.1.0 and changelog 2021-12-16 17:29:17 +01:00
J-Jamet
91a2bc3862 Fix virtual content 2021-12-16 17:25:41 +01:00
J-Jamet
78a8a840b0 Add path in search result 2021-12-16 17:20:33 +01:00
J-Jamet
f4d54b6ca3 Fix margin 2021-12-16 16:54:53 +01:00
J-Jamet
bc7a1c332c Breadcrunb toolbar animation and remove back button 2021-12-16 16:49:56 +01:00
J-Jamet
0e75cb9095 Change parallax 2021-12-16 16:23:09 +01:00
J-Jamet
41b6fb6dcd Add selectable background 2021-12-16 13:23:18 +01:00
J-Jamet
2ca3cbc88f Fix database title 2021-12-16 13:12:08 +01:00
J-Jamet
d05641a3d6 Change toolbar parallax 2021-12-16 13:02:04 +01:00
J-Jamet
28bf84e05c Fix search 2021-12-16 12:56:14 +01:00
J-Jamet
ff51b53660 Back group in breadcrumb 2021-12-16 12:25:25 +01:00
J-Jamet
8b8e034b18 Refresh breadcrumb 2021-12-16 12:06:42 +01:00
J-Jamet
39927b06e3 Change breadcrumb UI 2021-12-16 12:01:26 +01:00
J-Jamet
66db2e7d16 Better breadcrumb implementation 2021-12-16 11:26:17 +01:00
Kunzisoft
a927c33ef1 Translated using Weblate (French)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-12-15 12:50:48 +01:00
J-Jamet
17bc18b881 Add breadcrumb for group 2021-12-13 14:24:12 +01:00
Oymate
aa643c4a82 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 7.7% (44 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2021-12-12 07:52:01 +01:00
J-Jamet
836fbea676 Merge tag '3.0.4' into develop
3.0.4
2021-12-09 20:11:08 +01:00
J-Jamet
045049243c Merge branch 'release/3.0.4' 2021-12-09 20:10:57 +01:00
J-Jamet
b9813a3494 Upgrade version code 2021-12-09 19:54:12 +01:00
J-Jamet
9b42a93ce1 Change lock button 2021-12-09 19:49:38 +01:00
J-Jamet
8502bceef1 Fix search 2021-12-09 18:38:38 +01:00
J-Jamet
663387476f Change select entry min height 2021-12-09 14:33:07 +01:00
J-Jamet
daafd83df9 Better extra key implementation 2021-12-09 14:29:41 +01:00
J-Jamet
f780f2725b Fix compat inline suggestions request 2021-12-09 14:15:40 +01:00
J-Jamet
483aca871a Upgrade to version 3.0.4 and fix inline autofill suggestion #1165 2021-12-09 13:38:42 +01:00
J-Jamet
352e709c3b Merge branch 'master' into develop 2021-12-09 11:53:40 +01:00
J-Jamet
629057b2c1 Fix autofill exception #1173 2021-12-09 11:46:39 +01:00
J-Jamet
0e5f53596d Merge tag '3.0.3' into develop
3.0.3
2021-12-08 17:58:49 +01:00
Ihor Hordiichuk
35c8ea22b1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-12-08 03:51:55 +01:00
Óscar Fernández Díaz
23a548f9b4 Translated using Weblate (Spanish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-12-02 19:54:32 +01:00
Milo Ivir
7169b15fd8 Translated using Weblate (Croatian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-11-29 16:54:00 +01:00
abidin toumi
f9def8c96f Translated using Weblate (Arabic)
Currently translated at 78.7% (448 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-11-29 16:53:59 +01:00
Mr-Update
ef43837af1 Translated using Weblate (German)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-11-25 10:50:58 +01:00
random r
0979ca607d Translated using Weblate (Italian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-11-23 15:40:37 +01:00
Beytullah AKYÜZ
98fb27f77d Translated using Weblate (Turkish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-11-22 14:52:03 +01:00
JY3
d68510bbaa Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-11-22 14:52:03 +01:00
SC
4177d34b00 Translated using Weblate (Portuguese)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-11-19 19:52:59 +01:00
Serdar Sağlam
3ec5c04bf6 Translated using Weblate (Turkish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-11-19 19:52:59 +01:00
Eric
a877c068b6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-11-19 19:52:58 +01:00
Ihor Hordiichuk
6a3db90c1e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-11-19 19:52:58 +01:00
solokot
a079e0d864 Translated using Weblate (Russian)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-11-19 19:52:58 +01:00
Matthaiks
719776d66e Translated using Weblate (Polish)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-11-19 19:52:58 +01:00
Stephan Paternotte
c5af1241e9 Translated using Weblate (Dutch)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-11-19 19:52:57 +01:00
Retrial
27e4d7b563 Translated using Weblate (Greek)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-11-19 19:52:57 +01:00
VfBFan
450ab34721 Translated using Weblate (German)
Currently translated at 100.0% (569 of 569 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-11-19 19:52:57 +01:00
Hosted Weblate
3e2d4eae2c Merge branch 'origin/develop' into Weblate. 2021-11-17 20:33:06 +01:00
141 changed files with 3174 additions and 1171 deletions

View File

@@ -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]

View File

@@ -1,3 +1,17 @@
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

View File

@@ -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.

View File

@@ -3,16 +3,16 @@ apply plugin: 'kotlin-android'
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 = 92
versionName = "3.1.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -99,21 +99,21 @@ android {
}
}
def room_version = "2.3.0"
def room_version = "2.4.1"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.1.1'
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'
// 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.0'
implementation "com.google.android.material:material:$android_material_version"
// Database
implementation "androidx.room:room-runtime:$room_version"
@@ -123,7 +123,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

View File

@@ -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,12 +27,13 @@
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}" />

View File

@@ -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,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) {
SpecialMode.SELECTION -> {
// 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)
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchSelection(database, searchInfo)
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
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.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
}
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
}
SpecialMode.REGISTRATION -> {
// To 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)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
}
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.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
})

View File

@@ -36,7 +36,12 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
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
@@ -58,7 +63,11 @@ 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.MenuUtil
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,9 +77,10 @@ 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 entryProgress: LinearProgressIndicator? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var loadingView: ProgressBar? = null
@@ -93,7 +103,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,6 +123,7 @@ 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)
entryProgress = findViewById(R.id.entry_progress)
@@ -118,10 +134,19 @@ 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()
// Get Entry from UUID
try {
@@ -166,10 +191,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 +207,15 @@ 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 colors
mBackgroundColor = entryInfo.backgroundColor
mForegroundColor = entryInfo.foregroundColor
loadingView?.hideByFading()
mEntryLoaded = true
@@ -204,9 +227,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 +238,7 @@ class EntryActivity : DatabaseLockActivity() {
OtpType.TOTP -> {
entryProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
setProgressCompat(otpElement.secondsRemaining, true)
visibility = View.VISIBLE
}
}
@@ -252,13 +275,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(
@@ -304,6 +320,29 @@ 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) {
@@ -340,6 +379,7 @@ class EntryActivity : DatabaseLockActivity() {
if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
}
applyToolbarColors()
return super.onPrepareOptionsMenu(menu)
}

View File

@@ -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
@@ -243,6 +246,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

View File

@@ -25,7 +25,7 @@ import android.app.TimePickerDialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.*
import android.util.Log
import android.view.Menu
@@ -41,12 +41,15 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.fragments.GroupFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
@@ -60,6 +63,7 @@ import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -77,6 +81,7 @@ class GroupActivity : DatabaseLockActivity(),
GroupFragment.NodeClickListener,
GroupFragment.NodesActionMenuListener,
GroupFragment.OnScrollListener,
GroupFragment.GroupRefreshedListener,
SortDialogFragment.SortSelectionListener {
// Views
@@ -84,18 +89,25 @@ class GroupActivity : DatabaseLockActivity(),
private var coordinatorLayout: CoordinatorLayout? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var databaseNameContainer: ViewGroup? = null
private var databaseColorView: ImageView? = null
private var databaseNameView: TextView? = null
private var searchContainer: ViewGroup? = null
private var searchNumbers: TextView? = null
private var searchString: TextView? = null
private var toolbarBreadcrumb: Toolbar? = null
private var searchTitleView: View? = null
private var toolbarAction: ToolbarAction? = null
private var iconView: ImageView? = null
private var numberChildrenView: TextView? = null
private var addNodeButtonView: AddNodeButtonView? = null
private var groupNameView: TextView? = null
private var groupMetaView: TextView? = null
private var breadcrumbListView: RecyclerView? = null
private var loadingView: ProgressBar? = null
private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
private var mGroupFragment: GroupFragment? = null
private var mRecyclingBinEnabled = false
private var mRecyclingBinIsCurrentGroup = false
@@ -123,8 +135,6 @@ class GroupActivity : DatabaseLockActivity(),
AutofillHelper.buildActivityResultLauncher(this)
else null
private var mIconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -134,13 +144,18 @@ class GroupActivity : DatabaseLockActivity(),
// Initialize views
rootContainerView = findViewById(R.id.activity_group_container_view)
coordinatorLayout = findViewById(R.id.group_coordinator)
iconView = findViewById(R.id.group_icon)
numberChildrenView = findViewById(R.id.group_numbers)
addNodeButtonView = findViewById(R.id.add_node_button)
toolbar = findViewById(R.id.toolbar)
databaseNameContainer = findViewById(R.id.database_name_container)
databaseColorView = findViewById(R.id.database_color)
databaseNameView = findViewById(R.id.database_name)
searchContainer = findViewById(R.id.search_container)
searchNumbers = findViewById(R.id.search_numbers)
searchString = findViewById(R.id.search_string)
toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb)
searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name)
groupMetaView = findViewById(R.id.group_meta)
breadcrumbListView = findViewById(R.id.breadcrumb_list)
toolbarAction = findViewById(R.id.toolbar_action)
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
@@ -152,10 +167,42 @@ class GroupActivity : DatabaseLockActivity(),
toolbar?.title = ""
setSupportActionBar(toolbar)
// Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
// Open group on breadcrumb click
onItemClickListener = { node, _ ->
// If last item & not a virtual root group
val currentGroup = mCurrentGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogToShowGroupInfo(currentGroup)
} else {
if (mGroupFragment?.nodeActionSelectionMode == true) {
finishNodeAction()
}
mDatabase?.let { database ->
onNodeClick(database, node)
}
}
}
onLongItemClickListener = { node, position ->
val currentGroup = mCurrentGroup
if (currentGroup != null && node == currentGroup
&& (currentGroup != mDatabase?.rootGroup
|| mDatabase?.rootGroupIsVirtual == false)
) {
finishNodeAction()
launchDialogForGroupUpdate(currentGroup)
} else {
onItemClickListener?.invoke(node, position)
}
}
}
breadcrumbListView?.apply {
adapter = mBreadcrumbAdapter
}
// Retrieve group if defined at launch
manageIntent(intent)
@@ -213,23 +260,21 @@ class GroupActivity : DatabaseLockActivity(),
// Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener {
GroupEditDialogFragment.create(GroupInfo().apply {
if (currentGroup.allowAddNoteInGroup) {
notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
launchDialogForGroupCreation(currentGroup)
}
addNodeButtonView?.setAddEntryClickListener {
mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(intent,
{
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentGroup.nodeId,
resultLauncher
)
mCurrentGroup?.nodeId?.let { currentParentGroupId ->
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentParentGroupId,
resultLauncher
)
}
}
},
{
@@ -282,9 +327,6 @@ class GroupActivity : DatabaseLockActivity(),
}
}
assignGroupViewElements(currentGroup)
invalidateOptionsMenu()
loadingView?.hideByFading()
}
@@ -335,6 +377,29 @@ class GroupActivity : DatabaseLockActivity(),
return rootContainerView
}
private fun loadGroup(database: Database?) {
when {
Intent.ACTION_SEARCH == intent.action -> {
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
database,
searchString,
PreferencesUtil.omitBackup(this)
)
}
mCurrentGroupState == null -> {
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
}
else -> {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
}
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
@@ -344,17 +409,23 @@ class GroupActivity : DatabaseLockActivity(),
&& database?.isRecycleBinEnabled == true
mRootGroup = database?.rootGroup
if (mCurrentGroupState == null) {
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
} else {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
loadGroup(database)
// Search suggestion
database?.let {
databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database)
val customColor = it.customColor
if (customColor != null) {
databaseColorView?.visibility = View.VISIBLE
databaseColorView?.setColorFilter(
customColor,
PorterDuff.Mode.SRC_IN
)
} else {
databaseColorView?.visibility = View.GONE
}
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it)
mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
mOnSuggestionListener = object : SearchView.OnSuggestionListener {
override fun onSuggestionClick(position: Int): Boolean {
mSearchSuggestionAdapter?.let { searchAdapter ->
@@ -429,16 +500,27 @@ class GroupActivity : DatabaseLockActivity(),
)
}
}
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
if (result.isSuccess) {
try {
if (mCurrentGroup == newNodes[0] as Group)
reloadCurrentGroup()
} catch (e: Exception) {
Log.e(
TAG,
"Unable to perform action after group update",
e
)
}
}
}
}
coordinatorLayout?.showActionErrorIfNeeded(result)
if (!result.isSuccess) {
reloadCurrentGroup()
}
finishNodeAction()
refreshNumberOfChildren(mCurrentGroup)
}
/**
@@ -463,16 +545,7 @@ class GroupActivity : DatabaseLockActivity(),
}
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
transformSearchInfoIntent(intent)
if (Intent.ACTION_SEARCH == intent.action) {
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
mDatabase,
searchString,
PreferencesUtil.omitBackup(this)
)
}
loadGroup(mDatabase)
}
}
@@ -492,62 +565,44 @@ class GroupActivity : DatabaseLockActivity(),
super.onSaveInstanceState(outState)
}
override fun onGroupRefreshed() {
mCurrentGroup?.let { currentGroup ->
assignGroupViewElements(currentGroup)
}
}
private fun assignGroupViewElements(group: Group?) {
// Assign title
if (group != null) {
if (groupNameView != null) {
val title = group.title
groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root)
groupNameView?.invalidate()
}
if (groupMetaView != null) {
val meta = group.nodeId.toString()
groupMetaView?.text = meta
if (meta.isNotEmpty()
&& !group.isVirtual
&& PreferencesUtil.showUUID(this)) {
groupMetaView?.visibility = View.VISIBLE
} else {
groupMetaView?.visibility = View.GONE
}
groupMetaView?.invalidate()
}
}
if (group?.isVirtual == true) {
searchTitleView?.visibility = View.VISIBLE
if (toolbar != null) {
toolbar?.navigationIcon = null
}
iconView?.visibility = View.GONE
searchContainer?.visibility = View.VISIBLE
val title = group.title
searchString?.text = if (title.isNotEmpty()) title else ""
searchNumbers?.text = group.numberOfChildEntries.toString()
databaseNameContainer?.visibility = View.GONE
toolbarBreadcrumb?.navigationIcon = null
toolbarBreadcrumb?.collapse()
} else {
searchTitleView?.visibility = View.GONE
// Assign the group icon depending of IconPack or custom icon
iconView?.visibility = View.VISIBLE
group?.let { currentGroup ->
iconView?.let { imageView ->
mIconDrawableFactory?.assignDatabaseIcon(
imageView,
currentGroup.icon,
mIconColor
)
}
if (toolbar != null) {
if (group.containsParent())
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
else {
toolbar?.navigationIcon = null
}
searchContainer?.visibility = View.GONE
databaseNameContainer?.visibility = View.VISIBLE
// Refresh breadcrumb
if (toolbarBreadcrumb?.isVisible != true) {
toolbarBreadcrumb?.expand {
setBreadcrumbNode(group)
}
} else {
// Add breadcrumb
setBreadcrumbNode(group)
}
}
// Assign number of children
refreshNumberOfChildren(group)
// Hide button
initAddButton(group)
invalidateOptionsMenu()
}
private fun setBreadcrumbNode(group: Group?) {
mBreadcrumbAdapter?.apply {
setNode(group)
breadcrumbListView?.scrollToPosition(itemCount -1)
}
}
private fun initAddButton(group: Group?) {
@@ -569,18 +624,6 @@ class GroupActivity : DatabaseLockActivity(),
}
}
private fun refreshNumberOfChildren(group: Group?) {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
text = group?.numberOfChildEntries?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
}
override fun onScrolled(dy: Int) {
if (actionNodeMode == null)
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
@@ -807,12 +850,7 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
when (node.type) {
Type.GROUP -> {
mOldGroupToUpdate = node as Group
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
.show(
supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP
)
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
@@ -829,6 +867,25 @@ class GroupActivity : DatabaseLockActivity(),
return true
}
private fun launchDialogToShowGroupInfo(group: Group) {
GroupDialogFragment.launch(group.getGroupInfo())
.show(supportFragmentManager, GroupDialogFragment.TAG_SHOW_GROUP)
}
private fun launchDialogForGroupCreation(group: Group) {
GroupEditDialogFragment.create(GroupInfo().apply {
if (group.allowAddNoteInGroup) {
notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
}
private fun launchDialogForGroupUpdate(group: Group) {
mOldGroupToUpdate = group
GroupEditDialogFragment.update(group.getGroupInfo())
.show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
}
override fun onCopyMenuClick(
database: Database,
nodes: List<Node>
@@ -1008,7 +1065,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
val lockButtonView = findViewById<View>(R.id.lock_button)
lockButtonView != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
lockButtonView,
@@ -1026,7 +1083,7 @@ class GroupActivity : DatabaseLockActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
// TODO change database
return true
}
R.id.menu_search ->
@@ -1081,22 +1138,6 @@ class GroupActivity : DatabaseLockActivity(),
}
}
@Suppress("DEPRECATION")
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH == intent.action) {
var flags = intent.flags
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
intent.flags = flags
}
super.startActivityForResult(intent, requestCode, options)
}
private fun removeSearch() {
intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) {

View File

@@ -21,7 +21,6 @@ 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
@@ -41,7 +40,6 @@ 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
@@ -101,18 +99,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
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)
@@ -139,7 +127,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
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 {
@@ -208,10 +195,19 @@ 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
@@ -249,8 +245,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
checkPermission()
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
@@ -510,7 +504,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
@@ -613,35 +606,6 @@ 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) {
@@ -729,8 +693,6 @@ 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) {

View File

@@ -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)
}
}
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* 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 com.kunzisoft.keepass.R
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 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 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)
}
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)
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)
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
}
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)
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)
}
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
}
}
}

View File

@@ -246,8 +246,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()

View File

@@ -99,6 +99,12 @@ class EntryEditFragment: DatabaseFragment() {
setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
}
setOnBackgroundColorClickListener {
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
}
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field)
}
@@ -147,6 +153,14 @@ class EntryEditFragment: DatabaseFragment() {
templateView.setIcon(iconImage)
}
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color ->
templateView.setBackgroundColor(color)
}
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color ->
templateView.setForegroundColor(color)
}
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
templateView.setPasswordField(passwordField)
}

View File

@@ -42,7 +42,6 @@ class EntryFragment: DatabaseFragment() {
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var uuidContainerView: View
private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null
@@ -88,7 +87,6 @@ class EntryFragment: DatabaseFragment() {
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 ->
@@ -200,7 +198,6 @@ class EntryFragment: DatabaseFragment() {
}
private fun assignUUID(uuid: UUID?) {
uuidView.text = uuid?.toString()
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}

View File

@@ -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,8 +149,8 @@ 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 (listActionNodes.contains(node)) {
@@ -195,7 +206,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?) {
@@ -260,6 +271,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} else {
notFoundView?.visibility = View.GONE
}
groupRefreshed?.onGroupRefreshed()
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
@@ -292,15 +305,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context))
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)
)
} else {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)
)
}
sortDialogFragment.show(childFragmentManager, "sortDialog")
@@ -447,6 +462,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
fun onScrolled(dy: Int)
}
interface GroupRefreshedListener {
fun onGroupRefreshed()
}
companion object {
private val TAG = GroupFragment::class.java.name
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -34,6 +34,7 @@ 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 +56,9 @@ import java.util.*
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
class NodeAdapter (private val context: Context,
private val database: Database)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
class NodesAdapter (private val context: Context,
private val database: Database)
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback
@@ -79,6 +80,8 @@ class NodeAdapter (private val context: Context,
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
@@ -87,9 +90,15 @@ class NodeAdapter (private val context: Context,
@ColorInt
private val mContentSelectionColor: Int
@ColorInt
private val mIconGroupColor: Int
private val mTextColorPrimary: Int
@ColorInt
private val mIconEntryColor: Int
private val mTextColor: Int
@ColorInt
private val mTextColorSecondary: Int
@ColorInt
private val mColorAccentLight: Int
@ColorInt
private val mTextColorInverse: Int
/**
* Determine if the adapter contains or not any element
@@ -110,12 +119,24 @@ class NodeAdapter (private val context: Context,
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 taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY)
taSelectionColor.recycle()
// To get text color for selection
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.mTextColorInverse = taSelectionTextColor.getColor(0, Color.WHITE)
taSelectionTextColor.recycle()
}
private fun assignPreferences() {
@@ -145,6 +166,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 +178,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
@@ -327,8 +355,8 @@ class NodeAdapter (private val context: Context,
val iconColor = if (holder.container.isSelected)
mContentSelectionColor
else when (subNode.type) {
Type.GROUP -> mIconGroupColor
Type.ENTRY -> mIconEntryColor
Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mTextColor
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
@@ -348,14 +376,24 @@ 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
}
// Specific elements for entry
if (subNode.type == Type.ENTRY) {
@@ -398,6 +436,50 @@ class NodeAdapter (private val context: Context,
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
// Assign colors
val backgroundColor = entry.backgroundColor
if (!holder.container.isSelected) {
if (backgroundColor != null) {
holder.container.setBackgroundColor(backgroundColor)
} else {
holder.container.setBackgroundColor(Color.TRANSPARENT)
}
} else {
holder.container.setBackgroundColor(mColorAccentLight)
}
val foregroundColor = entry.foregroundColor
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)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(
this,
subNode.icon,
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(mTextColorInverse)
holder.subText?.setTextColor(mTextColorInverse)
holder.otpToken?.setTextColor(mTextColorInverse)
holder.otpProgress?.setIndicatorColor(mTextColorInverse)
holder.attachmentIcon?.setColorFilter(mTextColorInverse)
holder.meta.setTextColor(mTextColorInverse)
}
database.stopManageEntry(entry)
}
@@ -430,15 +512,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 +580,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 +590,6 @@ class NodeAdapter (private val context: Context,
}
companion object {
private val TAG = NodeAdapter::class.java.name
private val TAG = NodesAdapter::class.java.name
}
}

View File

@@ -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?)

View File

@@ -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,44 +231,51 @@ object AutofillHelper {
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context,
database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem
) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
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(
// Build the content for IME UI
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 {
setTintBlendMode(BlendMode.DST)
})
setStartIcon(
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply {
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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build())
}
}

View File

@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
abstract fun nodeAction()
override fun onStartRun() {
nodeAction()
try {
nodeAction()
} catch (e: Exception) {
setError(e)
}
super.onStartRun()
}

View File

@@ -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)
}

View File

@@ -22,10 +22,10 @@ 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
@@ -226,31 +226,33 @@ class Database {
mDatabaseKDBX?.descriptionChanged = DateInstant()
}
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()
}
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)
}
}
val allowOTP: Boolean
@@ -364,7 +366,7 @@ class Database {
mDatabaseKDBX?.masterKey = masterKey
}
val rootGroup: Group?
var rootGroup: Group?
get() {
mDatabaseKDB?.rootGroup?.let {
return Group(it)
@@ -374,6 +376,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

View File

@@ -19,8 +19,10 @@
*/
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
@@ -238,6 +240,42 @@ 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)
}
}
private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty()
}
@@ -419,6 +457,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.expiryTime = expiryTime
entryInfo.url = url
entryInfo.notes = notes
entryInfo.backgroundColor = backgroundColor
entryInfo.foregroundColor = foregroundColor
entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
@@ -453,6 +493,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url
notes = newEntryInfo.notes
backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor
addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment ->

View File

@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.*
import kotlin.collections.ArrayList
@@ -308,8 +309,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)
@@ -453,6 +455,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

View File

@@ -44,15 +44,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
get() = "V1"
init {
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
rootGroup = createGroup().apply {
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
}
kdfListV3.add(KdfFactory.aesKdf)
}
private fun getGroupById(groupId: Int): GroupKDB? {
if (groupId == -1)
return null
return getGroupById(NodeIdInt(groupId))
}
val backupGroup: GroupKDB?
get() {
return retrieveBackup()
@@ -63,6 +61,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return listOf(BACKUP_FOLDER_TITLE)
}
var defaultUserName: String = ""
var color: Int? = null
override val kdfEngine: KdfEngine
get() = kdfListV3[0]
@@ -77,11 +79,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return list
}
val rootGroups: List<GroupKDB>
get() {
return rootGroup?.getChildGroups() ?: ArrayList()
}
override val passwordEncoding: String
get() = "ISO-8859-1"

View File

@@ -414,37 +414,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)
}
}

View File

@@ -67,7 +67,7 @@ abstract class DatabaseVersioned<
var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
abstract val version: String
@@ -89,6 +89,7 @@ abstract class DatabaseVersioned<
set(value) {
field = value
value?.let {
removeGroupIndex(it)
addGroupIndex(it)
}
}
@@ -198,12 +199,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 +214,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 +238,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 +250,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,10 +274,6 @@ abstract class DatabaseVersioned<
this.entryIndexes.remove(entry.nodeId)
}
fun numberOfEntries(): Int {
return entryIndexes.size
}
open fun clearCache() {
this.groupIndexes.clear()
this.entryIndexes.clear()

View File

@@ -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,18 +61,43 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
private var binaryDataId: Int? = null
// Determine if this is a MetaStream entry
val isMetaStream: Boolean
get() {
if (notes.isEmpty()) return false
if (binaryDescription != PMS_ID_BINDESC) return false
if (title.isEmpty()) return false
if (title != PMS_ID_TITLE) return false
if (username.isEmpty()) return false
if (username != PMS_ID_USER) return false
if (url.isEmpty()) return false
if (url != PMS_ID_URL) return false
return icon.standard.id == KEY_ID
}
fun isMetaStream(): Boolean {
if (notes.isEmpty()) return false
if (binaryDescription != PMS_ID_BINDESC) return false
if (title.isEmpty()) return false
if (title != PMS_ID_TITLE) return false
if (username.isEmpty()) return false
if (username != PMS_ID_USER) return false
if (url.isEmpty()) return false
if (url != PMS_ID_URL) return false
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()
@@ -184,6 +210,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 {

View File

@@ -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

View File

@@ -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 }
}
}
/**

View File

@@ -44,4 +44,6 @@ abstract class NodeId<Id> : Parcelable {
override fun hashCode(): Int {
return id?.hashCode() ?: 0
}
abstract fun toVisualString(): String?
}

View File

@@ -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> {

View File

@@ -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> {

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -311,8 +311,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 +336,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)
}
}
}

View File

@@ -20,6 +20,7 @@
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
@@ -30,7 +31,6 @@ 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.*
@@ -98,7 +98,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()
}
@@ -153,10 +153,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
@@ -303,7 +299,34 @@ class DatabaseInputKDB(cacheDirectory: File,
newGroup = null
}
newEntry?.let { entry ->
mDatabase.addEntryIndex(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
}

View File

@@ -68,7 +68,7 @@ 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)

View File

@@ -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 ->
GroupOutputKDB(group, outputStream).output()
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.buildNewAttachment()
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.buildNewAttachment()
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))
}
}

View File

@@ -48,7 +48,6 @@ 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

View File

@@ -119,7 +119,7 @@ class GroupActivityEducation(activity: Activity)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.tintTarget(false)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {

View File

@@ -38,9 +38,9 @@ class PasswordActivityEducation(activity: Activity)
activity.getString(R.string.education_unlock_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round))
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_lock_open_white_24dp))
.textColorInt(getTextColor())
.tintTarget(false)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {

View File

@@ -40,6 +40,8 @@ class EntryInfo : NodeInfo {
var password: String = ""
var url: String = ""
var notes: String = ""
var backgroundColor: Int? = null
var foregroundColor: Int? = null
var customFields: MutableList<Field> = mutableListOf()
var attachments: MutableList<Attachment> = mutableListOf()
var otpModel: OtpModel? = null
@@ -53,6 +55,10 @@ class EntryInfo : NodeInfo {
password = parcel.readString() ?: password
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
val readBgColor = parcel.readInt()
backgroundColor = if (readBgColor == -1) null else readBgColor
val readFgColor = parcel.readInt()
foregroundColor = if (readFgColor == -1) null else readFgColor
parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(attachments, Attachment::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
@@ -70,6 +76,8 @@ class EntryInfo : NodeInfo {
parcel.writeString(password)
parcel.writeString(url)
parcel.writeString(notes)
parcel.writeInt(backgroundColor ?: -1)
parcel.writeInt(foregroundColor ?: -1)
parcel.writeList(customFields)
parcel.writeList(attachments)
parcel.writeParcelable(otpModel, flags)
@@ -196,6 +204,8 @@ class EntryInfo : NodeInfo {
if (password != other.password) return false
if (url != other.url) return false
if (notes != other.notes) return false
if (backgroundColor != other.backgroundColor) return false
if (foregroundColor != other.foregroundColor) return false
if (customFields != other.customFields) return false
if (attachments != other.attachments) return false
if (otpModel != other.otpModel) return false
@@ -211,6 +221,8 @@ class EntryInfo : NodeInfo {
result = 31 * result + password.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + notes.hashCode()
result = 31 * result + backgroundColor.hashCode()
result = 31 * result + foregroundColor.hashCode()
result = 31 * result + customFields.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0)

View File

@@ -1,12 +1,15 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
import java.util.*
class GroupInfo : NodeInfo {
var id: UUID? = null
var notes: String? = null
init {
@@ -16,11 +19,14 @@ class GroupInfo : NodeInfo {
constructor(): super()
constructor(parcel: Parcel): super(parcel) {
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
notes = parcel.readString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
val uuid = if (id != null) ParcelUuid(id) else null
parcel.writeParcelable(uuid, flags)
parcel.writeString(notes)
}
@@ -29,6 +35,7 @@ class GroupInfo : NodeInfo {
if (other !is GroupInfo) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (notes != other.notes) return false
return true
@@ -36,6 +43,7 @@ class GroupInfo : NodeInfo {
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (notes?.hashCode() ?: 0)
return result
}

View File

@@ -4,6 +4,8 @@ import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.*
open class NodeInfo() : Parcelable {

View File

@@ -452,7 +452,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
getString(R.string.show_uuid_key),
getString(R.string.enable_education_screens_key),
getString(R.string.reset_education_screens_key) -> {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = true
DATABASE_PREFERENCE_CHANGED = true
}
}
return super.onPreferenceTreeClick(preference)
@@ -516,6 +516,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
companion object {
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
var DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
var DATABASE_PREFERENCE_CHANGED = false
}
}

View File

@@ -30,8 +30,8 @@ import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -165,28 +165,19 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// Database default username
dbDefaultUsernamePref = findPreference(getString(R.string.database_default_username_key))
if (database.allowDefaultUsername) {
dbDefaultUsernamePref?.summary = database.defaultUsername
} else {
dbDefaultUsernamePref?.isEnabled = false
// TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
}
dbDefaultUsernamePref?.summary = database.defaultUsername
// Database custom color
dbCustomColorPref = findPreference(getString(R.string.database_custom_color_key))
if (database.allowCustomColor) {
dbCustomColorPref?.apply {
try {
color = Color.parseColor(database.customColor)
summary = database.customColor
} catch (e: Exception) {
color = DialogColorPreference.DISABLE_COLOR
summary = ""
}
dbCustomColorPref?.apply {
val customColor = database.customColor
if (customColor != null) {
color = customColor
summary = ChromaUtil.getFormattedColorString(customColor, false)
} else{
color = DialogColorPreference.DISABLE_COLOR
summary = ""
}
} else {
dbCustomColorPref?.isEnabled = false
// TODO dbGeneralPrefCategory?.removePreference(dbCustomColorPref)
}
// Version
@@ -348,12 +339,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
private val colorSelectedListener: ((Boolean, Int)-> Unit) = { enable, color ->
dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
if (enable) {
private val colorSelectedListener: ((Int?)-> Unit) = { color ->
if (color != null) {
dbCustomColorPref?.color = color
dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
} else {
dbCustomColorPref?.color = DialogColorPreference.DISABLE_COLOR
dbCustomColorPref?.summary = ""
}
}
@@ -426,7 +418,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (result.isSuccess) {
newColor
} else {
mDatabase?.customColor = oldColor
mDatabase?.customColor = Color.parseColor(oldColor)
oldColor
}
dbCustomColorPref?.summary = defaultColorToShow
@@ -681,6 +673,27 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
// To reload group when database settings are modified
when (preference?.key) {
getString(R.string.database_name_key),
getString(R.string.database_description_key),
getString(R.string.database_default_username_key),
getString(R.string.database_custom_color_key),
getString(R.string.database_data_compression_key),
getString(R.string.database_data_remove_unlinked_attachments_key),
getString(R.string.recycle_bin_enable_key),
getString(R.string.recycle_bin_group_key),
getString(R.string.templates_group_enable_key),
getString(R.string.templates_group_uuid_key),
getString(R.string.max_history_items_key),
getString(R.string.max_history_size_key) -> {
NestedAppSettingsFragment.DATABASE_PREFERENCE_CHANGED = true
}
}
return super.onPreferenceTreeClick(preference)
}
companion object {
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
}

View File

@@ -41,7 +41,7 @@ class DialogColorPreference @JvmOverloads constructor(context: Context,
}
override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_input_color
return R.layout.fragment_color_picker
}
companion object {

View File

@@ -30,27 +30,57 @@ import android.view.Window
import android.widget.CompoundButton
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.androidclearchroma.IndicatorMode
import com.kunzisoft.androidclearchroma.colormode.ColorMode
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.*
import com.kunzisoft.androidclearchroma.view.ChromaColorView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ColorPickerDialogFragment
import com.kunzisoft.keepass.database.element.Database
class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private lateinit var rootView: View
private lateinit var enableSwitchView: CompoundButton
private var chromaColorFragment: ChromaColorFragment? = null
private lateinit var chromaColorView: ChromaColorView
var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null
var onColorSelectedListener: ((color: Int?) -> Unit)? = null
private var mDefaultColor = Color.WHITE
private var mActivated = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
rootView = requireActivity().layoutInflater.inflate(R.layout.pref_dialog_input_color, null)
rootView = requireActivity().layoutInflater.inflate(R.layout.fragment_color_picker, null)
enableSwitchView = rootView.findViewById(R.id.switch_element)
chromaColorView = rootView.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
}
alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->
onDialogClosed(true)
@@ -68,8 +98,6 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
// request a window without the title
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
dialog.setOnShowListener { measureLayout(it as Dialog) }
return dialog
}
@@ -77,73 +105,48 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
super.onDatabaseRetrieved(database)
database?.let {
val initColor = try {
var initColor = it.customColor
if (initColor != null) {
enableSwitchView.isChecked = true
Color.parseColor(it.customColor)
} catch (e: Exception) {
} else {
enableSwitchView.isChecked = false
DEFAULT_COLOR
initColor = DEFAULT_COLOR
}
chromaColorView.currentColor = initColor
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
}
val fragmentManager = childFragmentManager
chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment?
if (chromaColorFragment == null) {
chromaColorFragment = newInstance(arguments)
fragmentManager.beginTransaction().apply {
add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS)
commit()
}
}
}
override fun onDialogClosed(database: Database?, positiveResult: Boolean) {
super.onDialogClosed(database, positiveResult)
if (positiveResult) {
val customColorEnable = enableSwitchView.isChecked
chromaColorFragment?.currentColor?.let { currentColor ->
onColorSelectedListener?.invoke(customColorEnable, currentColor)
database?.let {
val newColor = if (customColorEnable) {
ChromaUtil.getFormattedColorString(currentColor, false)
} else {
""
}
val oldColor = database.customColor
database.customColor = newColor
saveColor(oldColor, newColor)
}
val newColor: Int? = if (enableSwitchView.isChecked)
chromaColorView.currentColor
else
null
onColorSelectedListener?.invoke(newColor)
database?.let {
val oldColor = database.customColor
database.customColor = newColor
saveColor(oldColor, newColor)
}
}
}
/**
* Set new dimensions to dialog
* @param ad dialog
*/
private fun measureLayout(ad: Dialog) {
val typedValue = TypedValue()
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true)
val heightMultiplier = typedValue.float
val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt()
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true)
val widthMultiplier = typedValue.float
val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt()
ad.window?.setLayout(width, height)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return rootView
}
companion object {
private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS"
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"
@ColorInt
const val DEFAULT_COLOR: Int = Color.WHITE
@@ -151,9 +154,7 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
val fragment = DatabaseColorPreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK)
bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal)
bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal)
bundle.putInt(ARG_INITIAL_COLOR, DEFAULT_COLOR)
fragment.arguments = bundle
return fragment

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.activityViewModels
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -76,9 +77,17 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
// To inherit to save element in database
}
protected fun saveColor(oldColor: String,
newColor: String) {
mDatabaseViewModel.saveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
protected fun saveColor(oldColor: Int?,
newColor: Int?) {
val oldColorString = if (oldColor != null)
ChromaUtil.getFormattedColorString(oldColor, false)
else
""
val newColorString = if (newColor != null)
ChromaUtil.getFormattedColorString(newColor, false)
else
""
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
}
protected fun saveCompression(oldCompression: CompressionAlgorithm,

View File

@@ -19,7 +19,13 @@
*/
package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.app.AlarmManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
import android.util.Log
import android.view.View
import android.widget.NumberPicker
import com.kunzisoft.keepass.R
@@ -62,6 +68,30 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
}
}
override fun onResume() {
super.onResume()
(context?.applicationContext?.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& !alarmManager.canScheduleExactAlarms()) {
setExplanationText(R.string.warning_exact_alarm)
setExplanationButton(R.string.permission) {
// Open the exact alarm permission screen
try {
startActivity(Intent().apply {
action = ACTION_REQUEST_SCHEDULE_EXACT_ALARM
})
} catch (e: Exception) {
Log.e(TAG, "Unable to open exact alarm permission screen", e)
}
}
} else {
explanationText = ""
setExplanationButton("") {}
}
}
}
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
if (duration < 0) {
mEnabled = false
@@ -164,6 +194,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
}
companion object {
private const val TAG = "DurationDialogFrgCmpt"
private const val ENABLE_KEY = "ENABLE_KEY"
private const val DAYS_KEY = "DAYS_KEY"
private const val HOURS_KEY = "HOURS_KEY"

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
@@ -35,6 +36,7 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
private var inputTextView: EditText? = null
private var textUnitView: TextView? = null
private var textExplanationView: TextView? = null
private var explanationButton: Button? = null
private var switchElementView: CompoundButton? = null
private var mOnInputTextEditorActionListener: TextView.OnEditorActionListener? = null
@@ -100,6 +102,27 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
explanationText = getString(explanationTextId)
}
val explanationButtonText: String?
get() = explanationButton?.text?.toString() ?: ""
fun setExplanationButton(explanationButtonText: String?, clickListener: View.OnClickListener) {
explanationButton?.apply {
if (explanationButtonText != null && explanationButtonText.isNotEmpty()) {
text = explanationButtonText
visibility = View.VISIBLE
setOnClickListener(clickListener)
} else {
text = ""
visibility = View.GONE
setOnClickListener(null)
}
}
}
fun setExplanationButton(@StringRes explanationButtonTextId: Int, clickListener: View.OnClickListener) {
setExplanationButton(getString(explanationButtonTextId), clickListener)
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
@@ -128,6 +151,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
textUnitView?.visibility = View.GONE
textExplanationView = view.findViewById(R.id.explanation_text)
textExplanationView?.visibility = View.GONE
explanationButton = view.findViewById(R.id.explanation_button)
explanationButton?.visibility = View.GONE
switchElementView = view.findViewById(R.id.switch_element)
switchElementView?.visibility = View.GONE
}

View File

@@ -66,9 +66,26 @@ object TimeoutHelper {
val triggerTime = System.currentTimeMillis() + timeout
Log.d(TAG, "TimeoutHelper start")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& !alarmManager.canScheduleExactAlarms()) {
alarmManager.set(
AlarmManager.RTC,
triggerTime,
getLockPendingIntent(context)
)
} else {
alarmManager.setExact(
AlarmManager.RTC,
triggerTime,
getLockPendingIntent(context)
)
}
} else {
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
alarmManager.set(
AlarmManager.RTC,
triggerTime,
getLockPendingIntent(context)
)
}
}
}

View File

@@ -72,11 +72,29 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
)
// Launch the effective action after a small time
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
} else {
alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& !alarmManager.canScheduleExactAlarms()) {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
)
}
} else {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
)
}
}
} else {
cancelLockPendingIntent(context)

View File

@@ -46,6 +46,10 @@ abstract class TemplateAbstractView<
protected var headerContainerView: ViewGroup
protected var entryIconView: ImageView
protected var backgroundColorView: View
protected var foregroundColorView: View
protected var backgroundColorButton: ImageView
protected var foregroundColorButton: ImageView
private var titleContainerView: ViewGroup
protected var templateContainerView: ViewGroup
private var customFieldsContainerView: SectionView
@@ -57,6 +61,10 @@ abstract class TemplateAbstractView<
headerContainerView = findViewById(R.id.template_header_container)
entryIconView = findViewById(R.id.template_icon_button)
backgroundColorView = findViewById(R.id.template_background_color)
foregroundColorView = findViewById(R.id.template_foreground_color)
backgroundColorButton = findViewById(R.id.template_background_color_button)
foregroundColorButton = findViewById(R.id.template_foreground_color_button)
titleContainerView = findViewById(R.id.template_title_container)
templateContainerView = findViewById(R.id.template_fields_container)
// To fix card view margin below Marshmallow
@@ -396,37 +404,61 @@ abstract class TemplateAbstractView<
// Icon already populate
val titleView: TEntryFieldView? = findViewWithTag(FIELD_TITLE_TAG)
titleView?.value?.let {
mEntryInfo?.title = it
try {
val titleView: TEntryFieldView? = findViewWithTag(FIELD_TITLE_TAG)
titleView?.value?.let {
mEntryInfo?.title = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate title view", e)
}
val userNameView: TEntryFieldView? = findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value?.let {
mEntryInfo?.username = it
try {
val userNameView: TEntryFieldView? = findViewWithTag(FIELD_USERNAME_TAG)
userNameView?.value?.let {
mEntryInfo?.username = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate username view", e)
}
val passwordView: TEntryFieldView? = findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value?.let {
mEntryInfo?.password = it
try {
val passwordView: TEntryFieldView? = findViewWithTag(FIELD_PASSWORD_TAG)
passwordView?.value?.let {
mEntryInfo?.password = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate password view", e)
}
val urlView: TEntryFieldView? = findViewWithTag(FIELD_URL_TAG)
urlView?.value?.let {
mEntryInfo?.url = it
try {
val urlView: TEntryFieldView? = findViewWithTag(FIELD_URL_TAG)
urlView?.value?.let {
mEntryInfo?.url = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate url view", e)
}
val expirationView: TDateTimeView? = findViewWithTag(FIELD_EXPIRES_TAG)
expirationView?.activation?.let {
mEntryInfo?.expires = it
}
expirationView?.dateTime?.let {
mEntryInfo?.expiryTime = it
try {
val expirationView: TDateTimeView? = findViewWithTag(FIELD_EXPIRES_TAG)
expirationView?.activation?.let {
mEntryInfo?.expires = it
}
expirationView?.dateTime?.let {
mEntryInfo?.expiryTime = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate expiration view", e)
}
val notesView: TEntryFieldView? = findViewWithTag(FIELD_NOTES_TAG)
notesView?.value?.let {
mEntryInfo?.notes = it
try {
val notesView: TEntryFieldView? = findViewWithTag(FIELD_NOTES_TAG)
notesView?.value?.let {
mEntryInfo?.notes = it
}
} catch (e: Exception) {
Log.e(TAG, "Unable to populate notes view", e)
}
retrieveCustomFieldsFromView(templateFieldNotEmpty, retrieveDefaultValues)

View File

@@ -5,13 +5,17 @@ import android.os.Build
import android.util.AttributeSet
import android.view.View
import androidx.annotation.IdRes
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.*
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.otp.OtpEntryFields
import org.joda.time.DateTime
@@ -51,7 +55,53 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
fun setIcon(iconImage: IconImage) {
mEntryInfo?.icon = iconImage
populateIconMethod?.invoke(entryIconView, iconImage)
refreshIcon()
}
fun setOnBackgroundColorClickListener(onClickListener: OnClickListener) {
backgroundColorButton.setOnClickListener(onClickListener)
}
fun getBackgroundColor(): Int? {
return mEntryInfo?.backgroundColor
}
fun setBackgroundColor(color: Int?) {
applyBackgroundColor(color)
mEntryInfo?.backgroundColor = color
}
private fun applyBackgroundColor(color: Int?) {
if (color != null) {
backgroundColorView.background.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
backgroundColorView.visibility = View.VISIBLE
} else {
backgroundColorView.visibility = View.GONE
}
}
fun setOnForegroundColorClickListener(onClickListener: OnClickListener) {
foregroundColorButton.setOnClickListener(onClickListener)
}
fun getForegroundColor(): Int? {
return mEntryInfo?.foregroundColor
}
fun setForegroundColor(color: Int?) {
applyForegroundColor(color)
mEntryInfo?.foregroundColor = color
}
private fun applyForegroundColor(color: Int?) {
if (color != null) {
foregroundColorView.background.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP)
foregroundColorView.visibility = View.VISIBLE
} else {
foregroundColorView.visibility = View.GONE
}
}
override fun preProcessTemplate() {
@@ -196,6 +246,8 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
refreshIcon()
applyBackgroundColor(mEntryInfo?.backgroundColor)
applyForegroundColor(mEntryInfo?.foregroundColor)
return super.populateViewsWithEntryInfo(showEmptyFields)
}

View File

@@ -73,7 +73,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
private val valueView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_TextEntryItem)
R.style.KeepassDXStyle_TextAppearance_TextNode)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT).also {

View File

@@ -80,7 +80,8 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
mActionModeCallback = null
}
fun invalidateMenu() {
override fun invalidateMenu() {
super.invalidateMenu()
open()
mActionModeCallback?.onPrepareActionMode(actionMode, menu)
}

View File

@@ -23,9 +23,7 @@ import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.*
import android.text.Selection
import android.text.Spannable
import android.text.SpannableString
@@ -37,6 +35,7 @@ import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -44,6 +43,16 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import androidx.appcompat.view.menu.ActionMenuItemView
import android.widget.ImageView
import androidx.appcompat.widget.ActionMenuView
import androidx.core.graphics.drawable.DrawableCompat
import android.graphics.drawable.Drawable
import com.google.android.material.appbar.CollapsingToolbarLayout
/**
* Replace font by monospace, must be called after setText()
@@ -207,4 +216,50 @@ fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
}
}
}
fun Toolbar.changeControlColor(color: Int) {
val colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
for (i in 0 until childCount) {
val view: View = getChildAt(i)
// Change the color of back button (or open drawer button).
if (view is ImageView) {
//Action Bar back button
view.drawable.colorFilter = colorFilter
}
if (view is ActionMenuView) {
view.post {
for (j in 0 until view.childCount) {
// Change the color of any ActionMenuViews - icons that
// are not back button, nor text, nor overflow menu icon.
val innerView: View = view.getChildAt(j)
if (innerView is ActionMenuItemView) {
innerView.compoundDrawables.forEach { drawable ->
//Important to set the color filter in separate thread,
//by adding it to the message queue
//Won't work otherwise.
drawable?.colorFilter = colorFilter
}
}
}
}
}
}
// Change the color of title and subtitle.
setTitleTextColor(color)
setSubtitleTextColor(color)
// Change the color of the Overflow Menu icon.
var drawable: Drawable? = overflowIcon
if (drawable != null) {
drawable = DrawableCompat.wrap(drawable)
DrawableCompat.setTint(drawable.mutate(), color)
overflowIcon = drawable
}
invalidate()
}
fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
setCollapsedTitleTextColor(color)
setExpandedTitleColor(color)
invalidate()
}

View File

@@ -0,0 +1,15 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ColorPickerViewModel: ViewModel() {
val colorPicked : LiveData<Int?> get() = _colorPicked
private val _colorPicked = MutableLiveData<Int?>()
fun pickColor(color: Int?) {
_colorPicked.value = color
}
}

View File

@@ -14,6 +14,14 @@ abstract class NodeEditViewModel : ViewModel() {
val onIconSelected : LiveData<IconImage> get() = _onIconSelected
private val _onIconSelected = SingleLiveEvent<IconImage>()
private var mColorRequest: ColorRequest = ColorRequest.BACKGROUND
val requestColorSelection : LiveData<Int?> get() = _requestColorSelection
private val _requestColorSelection = SingleLiveEvent<Int?>()
val onBackgroundColorSelected : LiveData<Int?> get() = _onBackgroundColorSelected
private val _onBackgroundColorSelected = SingleLiveEvent<Int?>()
val onForegroundColorSelected : LiveData<Int?> get() = _onForegroundColorSelected
private val _onForegroundColorSelected = SingleLiveEvent<Int?>()
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
val onDateSelected : LiveData<DataDate> get() = _onDateSelected
@@ -29,6 +37,23 @@ abstract class NodeEditViewModel : ViewModel() {
_onIconSelected.value = iconImage
}
fun requestBackgroundColorSelection(initialColor: Int?) {
mColorRequest = ColorRequest.BACKGROUND
_requestColorSelection.value = initialColor
}
fun requestForegroundColorSelection(initialColor: Int?) {
mColorRequest = ColorRequest.FOREGROUND
_requestColorSelection.value = initialColor
}
fun selectColor(color: Int?) {
when (mColorRequest) {
ColorRequest.BACKGROUND -> _onBackgroundColorSelected.value = color
ColorRequest.FOREGROUND -> _onForegroundColorSelected.value = color
}
}
fun requestDateTimeSelection(dateInstant: DateInstant) {
_requestDateTimeSelection.value = dateInstant
}
@@ -40,4 +65,8 @@ abstract class NodeEditViewModel : ViewModel() {
fun selectTime(hours: Int, minutes: Int) {
_onTimeSelected.value = DataTime(hours, minutes)
}
private enum class ColorRequest {
BACKGROUND, FOREGROUND
}
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccentLight" android:state_pressed="true" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?attr/colorAccent" android:state_enabled="true" />
</selector>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid android:color="?attr/colorAccent"/>
</shape>
</item>
</ripple>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape>
<stroke
android:width="1dp"
android:color="?attr/textColorInverse" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid
android:color="@color/transparent"/>
</shape>
</item>
</ripple>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true">
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<padding
android:left="4dp"
android:right="12dp"
android:top="18dp"
android:bottom="8dp"/>
<solid android:color="@color/orange_light"/>
</shape>
</item>
<item>
<shape>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<padding
android:left="4dp"
android:right="12dp"
android:top="18dp"
android:bottom="8dp"/>
<solid android:color="@color/orange"/>
</shape>
</item>
</selector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/white">
<item>
<shape>
<stroke
android:width="1dp"
android:color="@color/white_grey" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"/>
<solid
android:color="@color/transparent"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#ffffff"
android:strokeWidth="0.99630105"
android:pathData="M 10,4 C 10,4 3.4004241,12.16731 4.0449219,16 4.4765841,18.566843 7.4158222,21 10,21 12.584196,21 15.523434,18.566843 15.955078,16 16.599576,12.16731 10,4 10,4 Z m 4,0 c 0,0 -0.59708,0.7527282 -1.386719,1.8476562 0.462543,0.6601114 0.953468,1.3798579 1.455078,2.1855469 0.803823,1.2911023 1.594188,2.7131219 2.15625,4.1015629 0.562116,1.388441 0.935799,2.740544 0.71875,4.03125 -0.268817,1.598597 -1.257809,3.011777 -2.517578,4.064453 -0.31833,0.265996 -0.657402,0.509805 -1.009765,0.726562 C 13.611687,20.98319 13.807101,21 14,21 16.584196,21 19.523434,18.566843 19.955078,16 20.599576,12.16731 14,4 14,4 Z m -4,2.5 c 0,0 0.497912,0.6283173 1.144531,1.53125 C 9.4508061,10.655343 7.697485,13.933867 8.0449219,16 8.262611,17.29447 9.126046,18.542028 10.253906,19.470703 10.169439,19.477241 10.083657,19.5 10,19.5 8.0238755,19.5 5.7754103,17.638661 5.4453125,15.675781 4.9524592,12.744903 10,6.5 10,6.5 Z" />
</group>
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#ffffff"
android:strokeWidth="0.99630105"
android:pathData="M 10,4 C 10,4 3.4004241,12.16731 4.0449219,16 4.4765841,18.566843 7.4158222,21 10,21 12.584196,21 15.523434,18.566843 15.955078,16 16.599576,12.16731 10,4 10,4 Z m 4,0 c 0,0 -0.597156,0.7527282 -1.386719,1.8476562 0.284669,0.4062608 0.575588,0.8441093 0.882813,1.3125 C 13.714676,6.8748215 14,6.5 14,6.5 c 0,0 5.04754,6.244903 4.554688,9.175781 -0.249068,1.481044 -1.591665,2.900275 -3.083985,3.509766 -0.31745,0.376905 -0.666337,0.728573 -1.044922,1.044922 -0.31833,0.265996 -0.657402,0.509805 -1.009765,0.726562 C 13.61168,20.98319 13.807101,21 14,21 16.584196,21 19.523434,18.566843 19.955078,16 20.599576,12.16731 14,4 14,4 Z" />
</group>
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<group
android:scaleX="0.8"
android:scaleY="0.8"
android:pivotX="12"
android:pivotY="12">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</group>
</vector>

View File

@@ -29,7 +29,8 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_parallax_height">
android:layout_height="@dimen/toolbar_parallax_height"
android:background="?attr/colorPrimary">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
@@ -138,8 +139,8 @@
<include
layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -100,8 +100,8 @@
<include
layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />

View File

@@ -31,11 +31,80 @@
android:layout_height="wrap_content"
android:theme="?attr/toolbarSpecialAppearance" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:title="@string/app_name"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_below="@+id/special_mode_view"
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance" >
<LinearLayout
android:id="@+id/database_name_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/database_color"
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/background_rounded_square"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:contentDescription="@string/content_description_database_color"/>
<TextView
android:id="@+id/database_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Database"
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary" />
</LinearLayout>
<RelativeLayout
android:id="@+id/search_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<TextView
android:id="@+id/search_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/search_results"
style="@style/KeepassDXStyle.TextAppearance.Default.TextOnPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/search_numbers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3"
android:layout_margin="4dp"
android:layout_below="@+id/search_title"
style="@style/KeepassDXStyle.TextAppearance.Info" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/search_string"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/search"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginTop="2dp"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_below="@+id/search_title"
android:layout_toRightOf="@+id/search_numbers"
android:layout_toEndOf="@+id/search_numbers"
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary" />
</RelativeLayout>
</androidx.appcompat.widget.Toolbar>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/group_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/special_mode_view"
android:layout_below="@+id/toolbar"
android:layout_above="@+id/toolbar_action">
<com.google.android.material.appbar.AppBarLayout
@@ -43,84 +112,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:targetApi="lollipop"
android:elevation="4dp"
android:fitsSystemWindows="true">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:title="@string/app_name"
android:id="@+id/toolbar_breadcrumb"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance"
android:elevation="4dp"
app:layout_scrollFlags="scroll|snap|enterAlways"
tools:targetApi="lollipop">
<LinearLayout
android:id="@+id/group_header"
android:layout_width="wrap_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/breadcrumb_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/toolbar"
android:orientation="vertical">
<TextView android:id="@+id/search_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/search_results"
android:visibility="gone"
style="@style/KeepassDXStyle.TextAppearance.Default.TextOnPrimary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:scaleType="fitXY" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_numbers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3"
style="@style/KeepassDXStyle.TextAppearance.Info" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="start|center_vertical"
android:layout_marginLeft="14dp"
android:layout_marginStart="14dp"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/root"
android:maxLines="2"
android:ellipsize="end"
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_meta"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C"
android:lines="1"
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Meta.TextOnPrimary" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="horizontal" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
@@ -132,6 +139,7 @@
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_below="@+id/toolbar">
<FrameLayout
android:id="@+id/nodes_list_fragment_container"
android:layout_width="match_parent"
@@ -158,6 +166,7 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
@@ -168,8 +177,8 @@
<include
layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
</RelativeLayout>

View File

@@ -59,8 +59,8 @@
<include
layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -44,7 +44,8 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:background="?attr/colorPrimary">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
@@ -57,7 +58,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="144dp"
android:layout_marginTop="?attr/actionBarSize">
android:layout_marginTop="?attr/actionBarSize"
android:background="?attr/colorPrimary">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
@@ -219,6 +221,7 @@
android:id="@+id/activity_password_info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/activity_password_info_text"

View File

@@ -42,8 +42,8 @@
<include
layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="start|bottom" />

View File

@@ -35,6 +35,6 @@
android:layout_margin="@dimen/button_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/file_browser" />
android:text="@string/file_browser"
style="@style/Widget.AppCompat.Button.Small"/>
</LinearLayout>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Jeremy Jamet / Kunzisoft.
Copyright 2022 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
@@ -17,8 +17,7 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/edit"
@@ -27,10 +26,13 @@
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<FrameLayout
android:id="@+id/color_dialog_container"
<com.kunzisoft.androidclearchroma.view.ChromaColorView
android:id="@+id/chroma_color_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="300dp"
app:chromaInitialColor="@color/white"
app:chromaColorMode="RGB"
app:chromaIndicatorMode="HEX"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
@@ -41,7 +43,7 @@
android:layout_margin="20dp"
android:text="@string/enable"
android:background="@drawable/background_button_small"
android:textColor="?attr/textColorInverse"
android:textColor="@color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:minHeight="48dp"/>

View File

@@ -77,7 +77,7 @@
android:id="@+id/entry_created"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
<!-- Modified -->
<androidx.appcompat.widget.AppCompatTextView
@@ -90,7 +90,7 @@
android:id="@+id/entry_modified"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
<!-- Accessed -->
<androidx.appcompat.widget.AppCompatTextView
@@ -127,16 +127,6 @@
android:layout_height="wrap_content"
android:text="@string/entry_UUID"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_UUID"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
</HorizontalScrollView>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -145,7 +135,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
</HorizontalScrollView>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
@@ -17,38 +17,128 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nodes_list"
android:contentDescription="@string/content_description_node_children"
android:scrollbars="vertical"
android:scrollbarStyle="insideOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
android:paddingBottom="?attr/actionBarSize"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/not_found_container"
android:layout_gravity="center"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/not_found_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/img_not_found"/>
<TextView
android:id="@+id/not_found_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_results"/>
</LinearLayout>
</FrameLayout>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary">
<LinearLayout
android:id="@+id/title_block"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/default_margin"
android:background="@drawable/background_repeat"
android:gravity="center"
style="@style/KeepassDXStyle.TextAppearance.Default">
<!-- Icon -->
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/default_margin"
android:src="@drawable/ic_blank_32dp"
style="@style/KeepassDXStyle.Icon"/>
<!-- Name -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
style="@style/KeepassDXStyle.Expanded.Title" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/default_margin"
tools:targetApi="o">
<!-- Note -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_note_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_notes"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
<!-- Expiration -->
<com.kunzisoft.keepass.view.DateTimeFieldView
android:id="@+id/group_expiration"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- Created -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_created_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_created"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_created"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
<!-- Modified -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_modified_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_modified"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_modified"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
<!-- UUID -->
<LinearLayout
android:id="@+id/group_UUID_container"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_UUID_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_UUID"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_UUID_reference"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
</HorizontalScrollView>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nodes_list"
android:contentDescription="@string/content_description_node_children"
android:scrollbars="vertical"
android:scrollbarStyle="insideOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
android:paddingBottom="?attr/actionBarSize"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/not_found_container"
android:layout_gravity="center"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/not_found_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/img_not_found"/>
<TextView
android:id="@+id/not_found_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_results"/>
</LinearLayout>
</FrameLayout>

View File

@@ -54,7 +54,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_title"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
style="@style/KeepassDXStyle.TextAppearance.TextNode"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toStartOf="@+id/item_attachment_size_container"

View File

@@ -18,7 +18,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
android:minHeight="36dp"
android:minHeight="48dp"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/breadcrumb_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
android:background="?android:attr/selectableItemBackground">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginStart="6dp"
android:layout_marginLeft="6dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/hint_icon_name"
android:scaleType="fitXY" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/root"
android:maxLines="2"
android:ellipsize="end"
style="@style/KeepassDXStyle.TextAppearance.SubTitle.TextOnPrimary"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_arrow_right_white_24dp"
android:tint="?attr/textColorInverse"
android:importantForAccessibility="no"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
style="@style/KeepassDXStyle.TextAppearance.SubTitle.TextOnPrimary"
android:textStyle="bold"
tools:targetApi="jelly_bean" />
</LinearLayout>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/breadcrumb_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
android:background="?android:attr/selectableItemBackground">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:gravity="center_vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:scaleType="fitXY" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_numbers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3"
style="@style/KeepassDXStyle.TextAppearance.Info" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="start|center_vertical"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/root"
android:maxLines="2"
android:ellipsize="end"
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_meta"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C"
android:lines="1"
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Meta.TextOnPrimary" />
</LinearLayout>
</LinearLayout>

View File

@@ -22,7 +22,7 @@
android:id="@+id/icon_container"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@drawable/background_item_selection">
style="@style/KeepassDXStyle.Selectable.Item">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_image"
android:layout_width="wrap_content"

View File

@@ -29,7 +29,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_last_modified"
tools:text = "Last Modified"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
style="@style/KeepassDXStyle.TextAppearance.TextNode"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -38,7 +38,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_title"
tools:text = "Title"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
style="@style/KeepassDXStyle.TextAppearance.TextNode"
android:maxLines="6"
android:ellipsize="end"
android:gravity="center"
@@ -49,7 +49,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_username"
tools:text = "Username"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
style="@style/KeepassDXStyle.TextAppearance.TextNode"
android:maxLines="6"
android:ellipsize="end"
android:gravity="center"

View File

@@ -27,52 +27,53 @@
style="@style/KeepassDXStyle.Selectable.Item">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:background="?android:attr/selectableItemBackground"
android:minHeight="48dp"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="?android:attr/selectableItemBackground" >
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/content_percent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="@dimen/image_list_margin_horizontal"
android:layout_marginBottom="@dimen/image_list_margin_horizontal"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginRight="@dimen/image_list_margin_vertical"
android:layout_marginEnd="@dimen/image_list_margin_vertical"
android:layout_gravity="center"
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginTop="@dimen/image_list_margin_horizontal"
android:layout_marginEnd="@dimen/image_list_margin_vertical"
android:layout_marginRight="@dimen/image_list_margin_vertical"
android:layout_marginBottom="@dimen/image_list_margin_horizontal"
android:scaleType="fitXY"
android:src="@drawable/ic_blank_32dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/node_container_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/node_icon"
app:layout_constraintLeft_toRightOf="@+id/node_icon"
app:layout_constraintEnd_toStartOf="@+id/node_options"
app:layout_constraintRight_toLeftOf="@+id/node_options">
app:layout_constraintLeft_toRightOf="@+id/node_icon"
app:layout_constraintRight_toLeftOf="@+id/node_options"
app:layout_constraintStart_toEndOf="@+id/node_icon"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_text"
@@ -101,32 +102,41 @@
android:lines="1"
android:singleLine="true"
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_path"
style="@style/KeepassDXStyle.TextAppearance.Entry.Meta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:visibility="gone"
tools:text="Database / Group A / Group B" />
</LinearLayout>
<LinearLayout
android:id="@+id/node_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
app:layout_constraintTop_toTopOf="parent"
android:gravity="end"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/node_otp_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:padding="4dp"
android:orientation="horizontal"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="?android:attr/selectableItemBackground"
app:layout_constraintTop_toTopOf="parent"
android:orientation="horizontal"
android:padding="4dp"
app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon"
app:layout_constraintEnd_toEndOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/node_otp_token"
@@ -135,20 +145,22 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="5136" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp">
<ProgressBar
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/node_otp_progress"
style="@style/KeepassDXStyle.ProgressBar.Circle.Secondary"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:max="100"
android:progress="60" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:indicatorSize="16dp"
app:trackThickness="2dp"
app:indicatorDirectionCircular="counterclockwise"
android:layout_gravity="center" />
</FrameLayout>
</LinearLayout>
@@ -170,12 +182,12 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginLeft="72dp"
android:layout_marginStart="72dp"
android:layout_marginRight="48dp"
android:layout_marginLeft="72dp"
android:layout_marginEnd="48dp"
app:layout_constraintBottom_toBottomOf="parent"/>
android:layout_marginRight="48dp"
android:background="?android:attr/listDivider"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,133 +17,155 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/edit"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/explanation_text"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:gravity="center"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_element"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/explanation_text" />
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/edit"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/switch_element">
<LinearLayout
android:id="@+id/duration_days_picker"
android:layout_width="wrap_content"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/explanation_text"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_width="0dp"
android:gravity="center"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@+id/duration_hours_picker"
app:layout_constraintRight_toLeftOf="@+id/duration_hours_picker">
<NumberPicker
android:id="@+id/days_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:src="@drawable/ic_day_white_24dp"
app:tint="?android:attr/textColor"
android:contentDescription="@string/digits" />
</LinearLayout>
<LinearLayout
android:id="@+id/duration_hours_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="HardcodedText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/duration_days_picker"
app:layout_constraintLeft_toRightOf="@+id/duration_days_picker"
app:layout_constraintEnd_toStartOf="@+id/duration_time_picker"
app:layout_constraintRight_toLeftOf="@+id/duration_time_picker">
<NumberPicker
android:id="@+id/hours_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textStyle="bold"
android:text=":" />
</LinearLayout>
<LinearLayout
android:id="@+id/duration_time_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="HardcodedText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/duration_hours_picker"
app:layout_constraintLeft_toRightOf="@+id/duration_hours_picker"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent">
<NumberPicker
android:id="@+id/minutes_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="28sp"
android:text="'"/>
<NumberPicker
android:id="@+id/seconds_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="28sp"
android:text="''"/>
</LinearLayout>
app:layout_constraintRight_toRightOf="parent"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<Button
android:id="@+id/explanation_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
tools:text="permission"
style="@style/Widget.AppCompat.Button.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/explanation_text" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_element"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable"
android:layout_marginTop="8dp"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/explanation_button" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/switch_element">
<LinearLayout
android:id="@+id/duration_days_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@+id/duration_hours_picker"
app:layout_constraintRight_toLeftOf="@+id/duration_hours_picker">
<NumberPicker
android:id="@+id/days_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:src="@drawable/ic_day_white_24dp"
app:tint="?android:attr/textColor"
android:contentDescription="@string/digits" />
</LinearLayout>
<LinearLayout
android:id="@+id/duration_hours_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="HardcodedText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/duration_days_picker"
app:layout_constraintLeft_toRightOf="@+id/duration_days_picker"
app:layout_constraintEnd_toStartOf="@+id/duration_time_picker"
app:layout_constraintRight_toLeftOf="@+id/duration_time_picker">
<NumberPicker
android:id="@+id/hours_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textStyle="bold"
android:text=":" />
</LinearLayout>
<LinearLayout
android:id="@+id/duration_time_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="HardcodedText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/duration_hours_picker"
app:layout_constraintLeft_toRightOf="@+id/duration_hours_picker"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent">
<NumberPicker
android:id="@+id/minutes_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="28sp"
android:text="'"/>
<NumberPicker
android:id="@+id/seconds_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="28sp"
android:text="''"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -1,48 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout
android:id="@+id/lock_button"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/lock_button_background"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_marginStart="-2dp"
android:layout_marginLeft="-2dp"
android:layout_marginBottom="-2dp"
tools:targetApi="lollipop"
android:elevation="4dp"
style="@style/KeepassDXStyle.Special.Button.Background"
android:layout_gravity="bottom|start"
android:contentDescription="@string/menu_lock" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/lock_button_stroke"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:layout_marginStart="-2dp"
android:layout_marginLeft="-2dp"
android:layout_marginBottom="-2dp"
tools:targetApi="lollipop"
android:elevation="4dp"
style="@style/KeepassDXStyle.Special.Button.Stroke"
android:layout_gravity="bottom|start"
android:contentDescription="@string/menu_lock" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/lock_button_icon"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
android:paddingBottom="6dp"
android:paddingTop="16dp"
android:paddingStart="4dp"
android:paddingLeft="4dp"
android:paddingEnd="12dp"
android:paddingRight="12dp"
tools:targetApi="lollipop"
android:elevation="4dp"
android:src="@drawable/ic_lock_white_24dp"
android:tint="@color/white"
android:layout_gravity="bottom|start"
android:contentDescription="@string/menu_lock" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_button"
style="@style/KeepassDXStyle.Fab.Special"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="mini"
android:layout_margin="8dp"
android:contentDescription="@string/lock"
android:src="@drawable/ic_lock_white_padding_24dp"
xmlns:app="http://schemas.android.com/apk/res-auto" />
</FrameLayout>

View File

@@ -26,6 +26,6 @@
android:id="@+id/date_time_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
style="@style/KeepassDXStyle.TextAppearance.TextNode" />
</LinearLayout>
</LinearLayout>

View File

@@ -20,7 +20,7 @@
android:focusable="false"
android:cursorVisible="false"
android:focusableInTouchMode="false"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
style="@style/KeepassDXStyle.TextAppearance.TextNode"
tools:text="2020-03-04 05:00" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.SwitchCompat

View File

@@ -17,9 +17,9 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:targetApi="o"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -29,30 +29,70 @@
android:id="@+id/template_header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/cardViewStyle"
android:visibility="gone">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
style="?attr/cardViewStyle">
<!-- Icon -->
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/template_icon_button"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/template_background_color"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_margin="9dp"
android:layout_gravity="top|end"
android:background="@drawable/background_rounded_square" />
<View
android:id="@+id/template_foreground_color"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_margin="32dp"
android:layout_gravity="top|end"
android:background="@drawable/background_rounded_square" />
<ImageView
android:id="@+id/template_background_color_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:src="@drawable/ic_blank_32dp"
android:contentDescription="@string/content_description_entry_icon"
android:layout_gravity="center"/>
android:layout_gravity="top|end"
android:layout_margin="3dp"
android:src="@drawable/ic_color_background_white_24dp"
android:contentDescription="@string/content_description_entry_background_color"
style="@style/KeepassDXStyle.ImageButton.Simple.Mini" />
<ImageView
android:id="@+id/template_foreground_color_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="25dp"
android:src="@drawable/ic_color_foreground_white_24dp"
android:contentDescription="@string/content_description_entry_background_color"
style="@style/KeepassDXStyle.ImageButton.Simple.Mini" />
<!-- Title -->
<FrameLayout
android:id="@+id/template_title_container"
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/card_view_padding">
<!-- Icon -->
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/template_icon_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_blank_32dp"
android:contentDescription="@string/content_description_entry_icon"/>
<!-- Title -->
<FrameLayout
android:id="@+id/template_title_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</FrameLayout>
</androidx.cardview.widget.CardView>
<LinearLayout

View File

@@ -88,7 +88,7 @@
<string name="space">المساحة</string>
<string name="search_label">البحث</string>
<string name="sort_menu">الفرز</string>
<string name="sort_ascending">تصاعدياً</string>
<string name="sort_ascending">تصاعدي</string>
<string name="sort_title">العنوان</string>
<string name="sort_username">اسم المستخدِم</string>
<string name="sort_creation_time">تاريخ الإنشاء</string>
@@ -472,7 +472,7 @@
<string name="error_import_app_properties">خطأ أثناء استيراد خصائص التطبيق</string>
<string name="error_export_app_properties">خطأ أثناء تصدير خصائص التطبيق</string>
<string name="warning_database_info_changed">غُيِّرت معلومات قاعدة البيانات من خارج هذا التطبيق.</string>
<string name="warning_database_info_changed_options">اكتب فوق التعديلات الخارجية عن طريق حفظ قاعدة البيانات أو أعد تحميلها لتضمين هذه التغييرات.</string>
<string name="warning_database_info_changed_options">اكتب فوق التعديلات الخارجية عن طريق حفظ قاعدة البيانات أو أعد تحميلها لجلب آخر التغييرات.</string>
<string name="open_advanced_unlock_prompt_store_credential">افتح محث فك القفل المتقدم لتخزين بيانات الاعتماد</string>
<string name="open_advanced_unlock_prompt_unlock_database">افتح محث فك القفل المتقدم لفتح قاعدة البيانات</string>
<string name="credential_before_click_advanced_unlock_button">اكتب كلمة السر، وأنقر هذا الزر.</string>
@@ -483,4 +483,10 @@
<string name="keyboard_previous_fill_in_title">إجراء لمس تلقائي</string>
<string name="keyboard_previous_lock_title">اقفل قاعدة البيانات</string>
<string name="education_advanced_unlock_title">فك القفل المتقدم لقاعدة البيانات</string>
<string name="hint_icon_name">اسم الأيقونة</string>
<string name="autofill_manual_selection_title">اختيار يدوي</string>
<string name="description_app_properties">خصائص KeePassDX لإدارة إعدادات التطبيقات</string>
<string name="warning_empty_recycle_bin">هل تريد حذف جميع العقد نهائيًا من سلة المهملات؟</string>
<string name="autofill_save_search_info_title">احفظ معلومات البحث</string>
<string name="autofill_ask_to_save_data_title">اسأل لحفظ البيانات</string>
</resources>

View File

@@ -29,4 +29,16 @@
<string name="discard">বাতিল</string>
<string name="validate">সত্যায়ন</string>
<string name="content_description_background">পটভূমি</string>
<string name="entry_cancel">বাতিল</string>
<string name="entry_notes">দ্রষ্টব্য</string>
<string name="entry_created">তৈরিকৃত</string>
<string name="entry_expires">মেয়াদ</string>
<string name="entry_UUID">বিশেষ শনাক্তকরণ সংকেত(ইউইউআইডি)</string>
<string name="entry_history">ইতিহাস</string>
<string name="entry_attachments">সংযুক্তি</string>
<string name="entry_keyfile">চাবিনথি(কিফাইল)</string>
<string name="entry_modified">পরিবর্তিত</string>
<string name="clipboard_cleared">ক্লিপবোর্ড পরিষ্কার করা হয়েছে</string>
<string name="entry_accessed">শেষ ব্যবহার</string>
<string name="clipboard_error">কিছু ডিভাইস অ্যাপ্লিকেশনগুলোকে ক্লিপবোর্ড ব্যবহার করতে দেয় না।</string>
</resources>

View File

@@ -602,4 +602,7 @@
<string name="autofill_select_entry">Vyberte položku…</string>
<string name="autofill_manual_selection_summary">Zobrazit možnosti umožňující uživateli si vybrat položku z databáze</string>
<string name="autofill_manual_selection_title">Ruční výběr</string>
<string name="hint_icon_name">Jméno symbolu</string>
<string name="warning_exact_alarm">Nepovolili jste aplikaci použít přesný alarm. Výsledkem je, že funkce požadující časovač nebudou provedeny v přesný okamžik.</string>
<string name="permission">Povolení</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More