Compare commits

...

307 Commits

Author SHA1 Message Date
J-Jamet
5872376f50 Merge branch 'release/2.9.14' 2021-03-20 11:42:33 +01:00
J-Jamet
075f72d9f6 Update version code 2021-03-18 15:48:58 +01:00
J-Jamet
4441ec1b14 Add small comments 2021-03-18 15:07:38 +01:00
J-Jamet
0735cc1a54 Condition to choose BinaryByte instead of BinaryFile 2021-03-18 14:58:56 +01:00
J-Jamet
dea2ad6904 Compress methods for byte array 2021-03-18 14:25:29 +01:00
J-Jamet
0f0b6b4a8a Better method to put binary 2021-03-18 13:37:33 +01:00
J-Jamet
174e562dcb Add BinaryByte and BinaryFile 2021-03-18 13:20:35 +01:00
J-Jamet
f080750545 Rename BinaryFile by BinaryData 2021-03-18 11:42:28 +01:00
J-Jamet
621415fe51 Replace BinaryFile length method 2021-03-18 11:40:01 +01:00
J-Jamet
428c2818a5 Upgrade version code 2021-03-17 22:17:37 +01:00
J-Jamet
5aa1c70999 Better binary hash implementation 2021-03-17 22:17:14 +01:00
J-Jamet
3508f47842 Externalize binary in custom icon 2021-03-17 15:31:12 +01:00
J-Jamet
62d4993e6d Better loaded cipher key implementation to prevent bad database file is key is not accessible 2021-03-17 13:56:18 +01:00
J-Jamet
631d946dcf Upgrade version code 2021-03-15 20:23:27 +01:00
J-Jamet
8945334f37 Fix reloading issue 2021-03-15 20:09:31 +01:00
J-Jamet
af2df11a56 Check binary length #924 2021-03-15 18:47:36 +01:00
J-Jamet
6dc0c42b1e Fix database save with bad binary #924 2021-03-15 18:44:53 +01:00
J-Jamet
0328293746 Upgrade version code to 61 2021-03-15 17:31:52 +01:00
J-Jamet
ad406947cf Check data in custom icon 2021-03-15 17:30:44 +01:00
J-Jamet
6bb4c1171f Better database stream exception caught 2021-03-15 17:02:13 +01:00
J-Jamet
faa39190fc Icon list optimization 2021-03-13 14:05:44 +01:00
J-Jamet
7c0b925c96 Rename BinaryStreamManager to BinaryDatabaseManager 2021-03-13 13:46:32 +01:00
J-Jamet
1b8c453fd0 Allow IconImage to have null custom icon 2021-03-13 13:06:40 +01:00
J-Jamet
8cc8f595bd upgrade version code 2021-03-13 12:26:10 +01:00
J-Jamet
9a5a8ae23a Downgrade material lib to 1.1.0 2021-03-12 14:00:10 +01:00
J-Jamet
02b27e235c Upgrade version code to upload a new beta version 2021-03-12 13:59:00 +01:00
J-Jamet
5874c5b9cb Resize image stream dynamically to show image preview #919 2021-03-11 16:02:15 +01:00
J-Jamet
fad09b2cd5 Better themes files organization 2021-03-11 11:01:16 +01:00
J-Jamet
cfb08afd7d Fix dateTime themes 2021-03-11 10:45:03 +01:00
J-Jamet
16d939c601 Fix dateTime dialog purple theme 2021-03-10 22:37:54 +01:00
J-Jamet
af072648c1 Fix reload database dialog when database created and when file modification is not 0 2021-03-10 21:24:00 +01:00
J-Jamet
45d2609494 Revert "Remove unused Base64 stream in temp file"
This reverts commit 4ecb8d4483.
2021-03-10 21:05:33 +01:00
J-Jamet
f5ea65f18c Change default icon width 2021-03-10 19:48:59 +01:00
J-Jamet
e9fc6bed23 Load icons as coroutine 2021-03-10 19:24:00 +01:00
J-Jamet
ccc8e4664d Update gradle 2021-03-10 17:43:11 +01:00
J-Jamet
651ef04137 Fix recognize iconId -1 2021-03-10 14:51:30 +01:00
Hosted Weblate
063aba333c Merge branch 'origin/develop' into Weblate. 2021-03-10 13:58:23 +01:00
J-Jamet
a3a517ff89 Fix reloading 2021-03-10 13:46:15 +01:00
Milo Ivir
40da29b681 Translated using Weblate (Croatian)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-03-09 19:03:13 +01:00
Oğuz Ersen
684d81c895 Translated using Weblate (Turkish)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-03-09 19:03:13 +01:00
Eric
c6ddd3b238 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-03-09 19:03:12 +01:00
Ihor Hordiichuk
3186413bee Translated using Weblate (Ukrainian)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-03-09 19:03:12 +01:00
solokot
aae1c4cf1c Translated using Weblate (Russian)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-03-09 19:03:11 +01:00
WaldiS
e64f264f12 Translated using Weblate (Polish)
Currently translated at 99.8% (519 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-03-09 19:03:11 +01:00
Retrial
08cf747f52 Translated using Weblate (Greek)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-03-09 19:03:11 +01:00
J-Jamet
383437a3c7 Fix search layout 2021-03-08 20:22:35 +01:00
J-Jamet
c7cba3f50b Fix notes in group 2021-03-08 20:07:16 +01:00
J-Jamet
7feb499d50 Update CHANGELOG 2021-03-08 19:11:15 +01:00
J-Jamet
2e6c25b651 Merge branch 'feature/Add_Group_Root' into develop 2021-03-08 19:09:58 +01:00
J-Jamet
192903e8d7 Add group to root 2021-03-08 19:09:21 +01:00
J-Jamet
6f72ade4d4 Change add node button view 2021-03-08 18:49:54 +01:00
J-Jamet
7e3fc0fa59 Fix 'kotlin-android-extensions' Gradle plugin deprecated 2021-03-08 18:22:43 +01:00
J-Jamet
4ea896b57c Fix small warning 2021-03-08 18:06:56 +01:00
J-Jamet
073ccb9b52 Revert androidx.fragment:fragment-ktx:1.2.5 2021-03-08 18:03:20 +01:00
J-Jamet
f32c944d31 Remove unused import 2021-03-08 17:50:06 +01:00
J-Jamet
acd1e3bdfc Refactoring virtual group 2021-03-08 17:49:10 +01:00
J-Jamet
774cbdf0fe Fix group search 2021-03-08 17:28:55 +01:00
J-Jamet
f5fd527590 Fix status bar color 2021-03-08 17:01:08 +01:00
J-Jamet
7ac9a7e94a Fix add button in a search 2021-03-08 16:26:43 +01:00
J-Jamet
5735f7a945 Better custom icons duplication implementation 2021-03-08 14:43:50 +01:00
J-Jamet
f8f423b5c1 Better XML writing for custom icons 2021-03-08 13:35:57 +01:00
J-Jamet
81ba7f0721 Prevent duplication during database saving 2021-03-08 13:29:02 +01:00
J-Jamet
e6be8c23fb Rollback toolbar home icon 2021-03-08 13:05:53 +01:00
J-Jamet
9cc1764a18 Consume icon added 2021-03-08 13:02:48 +01:00
J-Jamet
6a77adc313 Fix error after orientation change 2021-03-08 12:58:01 +01:00
J-Jamet
e532572d5a Prevent adding duplicate icon 2021-03-08 12:51:30 +01:00
zeritti
dcae49c5f8 Translated using Weblate (Czech)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-03-08 10:56:58 +01:00
Oliver Cervera
7321c01e8c Translated using Weblate (Italian)
Currently translated at 98.6% (513 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-03-08 08:16:00 +01:00
J-Jamet
a85b9998c3 Fix purple color 2021-03-07 19:10:56 +01:00
J-Jamet
30d2ce43d1 Update CHANGELOG 2021-03-07 19:00:41 +01:00
J-Jamet
d46edfc9b7 Fix flickering 2021-03-07 18:49:19 +01:00
J-Jamet
79d11138e6 Change home button icon 2021-03-07 18:28:55 +01:00
J-Jamet
f5cd019b6c Fix orientation change during icon selection 2021-03-07 17:58:31 +01:00
J-Jamet
42c4de56fd Encapsulate setResult 2021-03-07 17:33:46 +01:00
J-Jamet
44b3c28a2a Fix icon removed no longer accessible 2021-03-07 17:24:37 +01:00
J-Jamet
e5184a1568 Fix icon selection and icon removed no longer accessible 2021-03-07 17:15:48 +01:00
J-Jamet
76d60ded4c Fix color icon selection 2021-03-07 17:05:36 +01:00
J-Jamet
d2e7e925f7 Remove custom icons 2021-03-07 16:56:51 +01:00
J-Jamet
6357a30acb Upgrade material lib to 1.3.0 2021-03-06 14:57:44 +01:00
J-Jamet
4b1fdd0e38 Upgrade fragment lib 2021-03-06 14:49:30 +01:00
J-Jamet
227fc060b9 Fix restricted API and upgrade Autofill lib 2021-03-06 14:45:34 +01:00
J-Jamet
32e5aba906 Better color integration 2021-03-06 14:41:31 +01:00
J-Jamet
55013bb220 Change background dark color 2021-03-06 13:43:25 +01:00
J-Jamet
5544b20d7f Auto convert old themes 2021-03-06 13:28:10 +01:00
J-Jamet
d6c7f9c68b Fix button hidden in dialog #903 2021-03-06 12:53:44 +01:00
J-Jamet
8b5004e500 Change package import in dialog 2021-03-06 12:53:11 +01:00
J-Jamet
d6cf11b87d Fix file manager button 2021-03-06 12:35:07 +01:00
J-Jamet
d4c3a3be6b Center validate button in toolbar 2021-03-06 12:10:47 +01:00
J-Jamet
e724b188ef Remove unused code 2021-03-06 11:10:45 +01:00
J-Jamet
ebf92b1103 Remove unused code 2021-03-06 11:08:03 +01:00
J-Jamet
42e2a49af6 Better draw factory implementation 2021-03-06 10:48:05 +01:00
J-Jamet
be7cd3275a Better draw factory implementation 2021-03-06 10:22:14 +01:00
J-Jamet
966df11beb Fix subtitle toolbar color 2021-03-06 09:31:16 +01:00
J-Jamet
fad852f00d Fix icon standard color 2021-03-05 21:01:48 +01:00
Hosted Weblate
9c3e6eb823 Merge branch 'origin/develop' into Weblate. 2021-03-05 20:54:33 +01:00
J-Jamet
8b88f72efc OTP wiki link 2021-03-05 20:52:50 +01:00
J-Jamet
e0aab6cfbf Default custom tab when custom icon is already selected 2021-03-05 20:36:23 +01:00
J-Jamet
29a2e60e05 Fix fragment style 2021-03-05 20:06:30 +01:00
J-Jamet
12df74b3a7 Fix fragment style 2021-03-05 19:55:51 +01:00
Retrial
22d943c9e2 Translated using Weblate (Greek)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-03-05 19:50:50 +01:00
J-Jamet
5839f51f44 Fix otp key uppercase label #909 2021-03-05 15:14:52 +01:00
J-Jamet
4ecb8d4483 Remove unused Base64 stream in temp file 2021-03-05 14:52:56 +01:00
J-Jamet
a08035551a Move fragment in right package 2021-03-05 14:51:03 +01:00
J-Jamet
c2460d7262 Better code encapsulation 2021-03-05 14:46:23 +01:00
J-Jamet
4776eac07e Prevent custom icon usage with KDB database 2021-03-05 14:32:01 +01:00
J-Jamet
4952d107dd Upgrade NDK to v21 LTS 2021-03-05 13:17:03 +01:00
J-Jamet
b5d6ee9dee Fix education hints color 2021-03-05 13:13:07 +01:00
J-Jamet
e7a30c6024 Merge branch 'feature/Custom_Icons' into develop 2021-03-04 20:06:03 +01:00
J-Jamet
6578e52ec5 Fix error view in edit entry 2021-03-04 20:04:44 +01:00
J-Jamet
97508beb5c Icon error as warning 2021-03-04 19:50:00 +01:00
J-Jamet
e063b0d6fc Remove unused code 2021-03-04 19:48:13 +01:00
J-Jamet
bb65dc0e81 Change icon interface name 2021-03-04 19:41:43 +01:00
J-Jamet
09e00ec119 Better icon type implementation 2021-03-04 19:33:16 +01:00
J-Jamet
985f8fad3b Keep icon history 2021-03-04 19:00:17 +01:00
J-Jamet
a9accc8c42 Replace deprecated Password toggle 2021-03-04 18:34:15 +01:00
J-Jamet
41316d2bd3 Prevent add empty icon 2021-03-03 19:43:46 +01:00
J-Jamet
2c172eb8d3 Fix multiple addition 2021-03-03 19:30:33 +01:00
J-Jamet
d49827f9f8 Remove unused method 2021-03-03 17:38:45 +01:00
J-Jamet
b2aafda2b1 Auto select custom tab after upload an icon 2021-03-03 15:16:56 +01:00
J-Jamet
b4c50e0262 Async icon loading 2021-03-03 14:34:08 +01:00
J-Jamet
fbe51c12c1 Icon drawable as WeakReference 2021-03-03 12:32:06 +01:00
J-Jamet
991959416b Replace string section 2021-03-03 12:24:46 +01:00
J-Jamet
41f0e61f60 Scroll to the end when an icon is added 2021-03-02 16:55:41 +01:00
J-Jamet
eab8cd101f Prevent uploading icon in error 2021-03-02 16:50:35 +01:00
J-Jamet
97765d798c Remove apache commons collection 2021-03-02 16:23:13 +01:00
J-Jamet
a7f76248ac Better icon list implementation 2021-03-02 16:11:39 +01:00
J-Jamet
8de670fcf2 Fix uncaught exception 2021-03-02 14:48:15 +01:00
J-Jamet
744823cce4 Fix unique id for each binary file 2021-03-02 14:43:48 +01:00
J-Jamet
d95a3e00aa Resize icon file to upload 2021-03-02 13:59:15 +01:00
Deleted User
ccc190f7b0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 75.2% (386 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2021-03-02 07:03:29 +01:00
J-Jamet
3e6cd98cb9 Add big image file error 2021-03-01 17:54:10 +01:00
J-Jamet
0e3b8fdbb6 Upload custom attachment with thread 2021-03-01 16:39:46 +01:00
J-Jamet
5aa3f79616 Upload custom attachment and fix warning 2021-03-01 15:50:11 +01:00
vachan-maker
42427d0690 Translated using Weblate (Malayalam)
Currently translated at 74.8% (384 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2021-03-01 10:50:33 +01:00
J-Jamet
1a7b32e6d1 Select icon file 2021-03-01 10:48:38 +01:00
naofum
83555bfdc5 Translated using Weblate (Japanese)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-02-28 06:50:53 +01:00
J-Jamet
03990c1dd9 Add lock action 2021-02-25 19:46:51 +01:00
J-Jamet
b361be5cb0 Fix listener by using view model 2021-02-25 19:36:42 +01:00
J-Jamet
d02f6d1e67 Merge branch 'develop' into feature/Custom_Icons 2021-02-25 19:05:50 +01:00
J-Jamet
1e56c34e2f Fix ImageButton style 2021-02-25 19:05:31 +01:00
J-Jamet
2a2f8dcecd Migrate to ViewPager2 and as Activity 2021-02-25 18:54:42 +01:00
J-Jamet
b609ed3ad4 Fix number of icons 2021-02-25 15:44:23 +01:00
J-Jamet
80521f8ec2 Change list to recyclerview 2021-02-25 15:13:34 +01:00
J-Jamet
3a5df6a893 Add IconCustomFragment 2021-02-25 13:23:42 +01:00
Reza Almanda
0e60c4f910 Translated using Weblate (Indonesian)
Currently translated at 63.3% (325 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-02-24 23:50:38 +01:00
abidin toumi
f5f2d3c883 Translated using Weblate (Arabic)
Currently translated at 67.6% (347 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-02-24 23:50:36 +01:00
Kornelijus Tvarijanavičius
3c830bfaf2 Translated using Weblate (Lithuanian)
Currently translated at 22.4% (115 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2021-02-24 23:50:36 +01:00
random r
98caf9b5bf Translated using Weblate (Italian)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-02-24 23:50:35 +01:00
J-Jamet
cfbb8fab1b Merge branch 'develop' into feature/Custom_Icons 2021-02-24 21:18:08 +01:00
J-Jamet
3069e5e566 Change bioemtric unlock description #900 2021-02-24 21:13:32 +01:00
J-Jamet
ac050a09e8 Remove unused interface 2021-02-24 20:59:07 +01:00
J-Jamet
78406ccdbf Merge branch 'develop' into feature/Custom_Icons 2021-02-24 20:51:08 +01:00
J-Jamet
2efea1bb00 Merge branch 'feature/Refactor_Icons' into develop #96 2021-02-24 20:29:41 +01:00
J-Jamet
0157a160f0 Fix new entry icon inheritance 2021-02-24 20:27:19 +01:00
J-Jamet
eb4084a6a4 Factorize icon class 2021-02-24 20:19:25 +01:00
J-Jamet
63f5e5416f Refactor IconPool 2021-02-24 18:40:43 +01:00
J-Jamet
38e0433e8f Fix iconId #901 2021-02-22 21:25:14 +01:00
J-Jamet
fc5ffc5f62 Fix binary deduplication #715 2021-02-22 18:56:21 +01:00
J-Jamet
460e3558a9 First commit to create custom attachments 2021-02-22 17:17:31 +01:00
Vít Šindlář
b06f223124 Translated using Weblate (Czech)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-02-21 04:48:37 +01:00
Suyono Hermanto
aabfb2adfd Translated using Weblate (Indonesian)
Currently translated at 44.6% (229 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-02-20 17:50:38 +01:00
Kornelijus Tvarijanavičius
f9c47c9035 Translated using Weblate (Lithuanian)
Currently translated at 18.7% (96 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2021-02-20 17:50:38 +01:00
zeritti
e9485ebf56 Translated using Weblate (Czech)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-02-20 17:50:37 +01:00
J-Jamet
fcd7fd2889 Merge branch 'feature/Dark_Mode' into develop #714 2021-02-20 11:16:57 +01:00
J-Jamet
279f4a347a Update CHANGELOG 2021-02-20 11:16:46 +01:00
J-Jamet
b29fe23403 Fix clear style binaries color 2021-02-19 12:57:17 +01:00
J-Jamet
1972a551e9 Rename styles 2021-02-19 12:48:20 +01:00
J-Jamet
7e2ffd2ec4 Change Night word by Dark 2021-02-19 12:34:32 +01:00
J-Jamet
06793ae13e Fix database list elevation 2021-02-19 12:24:07 +01:00
J-Jamet
26e961d356 Fix color selection 2021-02-19 12:20:41 +01:00
J-Jamet
da956d3bd5 Change white colors 2021-02-19 12:01:36 +01:00
J-Jamet
318a72a123 Fix toolbar popup background 2021-02-19 11:46:42 +01:00
J-Jamet
5c4b4864d2 Setting to select theme brightness 2021-02-18 20:07:03 +01:00
J-Jamet
4ab31fe21a Small color change 2021-02-18 19:04:03 +01:00
J-Jamet
c22a213635 Fix title shadow 2021-02-18 18:39:28 +01:00
J-Jamet
aa166a0104 Fix windowLightStatusBar in v21 2021-02-18 18:34:38 +01:00
J-Jamet
2a36626731 Change blue 2021-02-18 18:22:43 +01:00
J-Jamet
2f2360fd48 Fix fab menu text color 2021-02-18 18:19:07 +01:00
J-Jamet
e466643229 Fix overflow color button 2021-02-18 18:09:47 +01:00
J-Jamet
a9044c3dc4 Fix white and black themes 2021-02-18 18:01:15 +01:00
J-Jamet
2d28cc21a0 Lock button with white color 2021-02-18 14:30:52 +01:00
J-Jamet
ebb0e7e118 Fix white & clear themes 2021-02-18 14:30:28 +01:00
J-Jamet
8205454858 Fix red & blue themes 2021-02-18 14:05:10 +01:00
ButterflyOfFire
e909112ab8 Translated using Weblate (Arabic)
Currently translated at 67.4% (346 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-02-18 13:50:31 +01:00
Stephan Paternotte
469923855a Translated using Weblate (Dutch)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-02-18 13:50:30 +01:00
Caetano Demián Moreno
f2aca08886 Translated using Weblate (Spanish)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-02-18 13:50:30 +01:00
J-Jamet
9900f8fecb Fix black theme 2021-02-18 13:48:33 +01:00
J-Jamet
73224887b9 Refactor toolbar popup theme 2021-02-18 13:41:29 +01:00
J-Jamet
e31574015b Refactor toolbar special appearance 2021-02-18 12:48:39 +01:00
J-Jamet
ef26251469 Refactor toolbar styles 2021-02-18 12:35:32 +01:00
J-Jamet
798bce2759 Fix custom bar home button color 2021-02-18 11:40:15 +01:00
J-Jamet
88fee5f6de Tint menu icon 2021-02-18 11:32:31 +01:00
J-Jamet
937695a1e5 First commit to implement Dark mode 2021-02-18 10:50:29 +01:00
J-Jamet
a43b580d67 Elevation in password view 2021-02-17 16:21:58 +01:00
J-Jamet
b8d0bff22b Remove unused style code 2021-02-17 15:00:24 +01:00
J-Jamet
c180d38394 Remove unused style code 2021-02-17 12:54:14 +01:00
J-Jamet
10f7d955ff Small style refactoring 2021-02-17 12:39:34 +01:00
J-Jamet
143651099a Auto open biometric prompt by default 2021-02-17 11:27:14 +01:00
J-Jamet
63bb12269f Update to version 2.9.14 2021-02-17 11:22:24 +01:00
J-Jamet
00ee80184e Merge tag '2.9.13' into develop
2.9.13
2021-02-15 12:47:54 +01:00
J-Jamet
60ba058515 Merge branch 'release/2.9.13' 2021-02-15 12:47:48 +01:00
Milo Ivir
1cf8131b6c Translated using Weblate (Croatian)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-02-13 17:50:46 +01:00
WaldiS
1b38bd59ef Translated using Weblate (Polish)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-02-13 17:50:46 +01:00
Oliver Cervera
2e409c3246 Translated using Weblate (Italian)
Currently translated at 99.2% (509 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-02-13 17:50:46 +01:00
J-Jamet
63fbca8029 Fix warning 2021-02-12 11:17:28 +01:00
J-Jamet
b37966f79c Remove empty translation 2021-02-12 11:03:06 +01:00
J-Jamet
bac5b0de5b Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-02-12 10:57:57 +01:00
J-Jamet
5dac161553 Small UI change 2021-02-12 10:53:48 +01:00
J-Jamet
528c167a88 Change delta reload time to 10 seconds to prevent dialog spamming #875 2021-02-12 10:37:43 +01:00
J-Jamet
cfe01aa996 Fix reloading database 2021-02-12 10:34:41 +01:00
J-Jamet
0f53c975cc Add popup theme to toolbar action 2021-02-11 19:24:43 +01:00
J-Jamet
b7b99c77c8 Add group edition scroll and notes multiline 2021-02-11 19:18:56 +01:00
J-Jamet
1eea5412a5 Add group expiration and fix bugs in group info 2021-02-11 19:13:30 +01:00
Oğuz Ersen
4240465930 Translated using Weblate (Turkish)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-02-11 17:57:54 +01:00
Eric
3dfbf7d2ad Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-02-11 17:57:48 +01:00
Ihor Hordiichuk
440490a4bb Translated using Weblate (Ukrainian)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-02-11 17:57:47 +01:00
solokot
746382811b Translated using Weblate (Russian)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-02-11 17:57:46 +01:00
Kunzisoft
967a54dd50 Translated using Weblate (French)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-02-11 17:57:45 +01:00
Retrial
289d9a2531 Translated using Weblate (Greek)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-02-11 17:57:44 +01:00
J-Jamet
c56b4964fe Add notes in group creation 2021-02-11 17:09:26 +01:00
J-Jamet
79dbb942f9 Add notes in groups #734 2021-02-11 16:05:58 +01:00
Hosted Weblate
a5bb5635d3 Merge branch 'origin/develop' into Weblate. 2021-02-11 12:32:26 +01:00
J-Jamet
3efe43c0fe Fix toolbar popup menu color 2021-02-11 12:10:11 +01:00
J-Jamet
5fb5299d34 Fix dialog color 2021-02-11 11:22:28 +01:00
J-Jamet
b988882251 Fix themes and add Purple Dark #889 2021-02-10 20:00:56 +01:00
J-Jamet
85e82e3fb9 Update CHANGELOG 2021-02-10 17:58:34 +01:00
J-Jamet
35cfe261d2 Change Regex to allow infinite OTP padding #585 2021-02-10 17:56:22 +01:00
J-Jamet
fe4faf9ebc Update CHANGELOG 2021-02-10 17:35:41 +01:00
J-Jamet
751392d656 Add cancellation to binary uploading 2021-02-10 17:28:00 +01:00
J-Jamet
2e5ce5e94f Merge branch 'feature/Image_Viewer' into develop #473 2021-02-10 11:49:41 +01:00
J-Jamet
535eeb2594 Prevent saving attachment if currently uploading 2021-02-10 11:47:20 +01:00
J-Jamet
8f5e0e93ee Fix view flickering 2021-02-09 21:33:00 +01:00
J-Jamet
6f17c5dcac Fix binary with KDB Database 2021-02-09 21:27:57 +01:00
J-Jamet
4e7c7ba8ce Fix output KDB 2021-02-09 20:20:46 +01:00
J-Jamet
6bd5b2345c Small refactoring code 2021-02-09 18:23:48 +01:00
Michalis
63832ef8fd Translated using Weblate (Greek)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-02-09 17:50:48 +01:00
J-Jamet
3719bf3593 Remove unused code 2021-02-09 17:27:46 +01:00
J-Jamet
7bca41ca72 Standardize readAllBytes methods 2021-02-09 14:21:47 +01:00
J-Jamet
3f6a9c3af5 Rollback readBytes method and default buffer to fix argon2 database 2021-02-09 14:15:18 +01:00
J-Jamet
309380bdd5 Perform binary preview animation 2021-02-08 19:40:29 +01:00
J-Jamet
f135bdb905 Fix click listener 2021-02-08 18:35:41 +01:00
J-Jamet
2b926fd157 Fix bad preview during upload 2021-02-08 18:28:22 +01:00
J-Jamet
e911eea69c Fix closing stream for preview 2021-02-08 18:25:29 +01:00
J-Jamet
9d182b8299 Change preview layout 2021-02-08 18:12:25 +01:00
J-Jamet
61366e000f Change image binary preview 2021-02-08 17:31:42 +01:00
J-Jamet
21cf49f4f8 Better preview state management 2021-02-08 16:30:19 +01:00
J-Jamet
30f0de83d3 Better viewer in list 2021-02-08 15:09:30 +01:00
J-Jamet
42278a4b66 Add image viewer toolbar 2021-02-08 14:17:47 +01:00
J-Jamet
8752f92cea Add thread to load images 2021-02-08 13:42:04 +01:00
J-Jamet
064c468e62 Merge branch 'feature/Encrypt_Temp_Binaries' into feature/Image_Viewer 2021-02-08 10:58:31 +01:00
Michalis
ef78fb749c Translated using Weblate (Greek)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-02-07 22:47:27 +01:00
J-Jamet
a5d1db392b Try to improve image thumbnail 2021-02-07 19:24:20 +01:00
J-Jamet
6234fc2ca3 Attachment viewer 2021-02-07 19:11:59 +01:00
J-Jamet
c9cf90cdc9 Add Copyright 2021-02-07 18:22:10 +01:00
J-Jamet
5b033975b6 Add Loope license directly in Loupe class 2021-02-07 18:14:49 +01:00
J-Jamet
5663a153f7 Merge branch 'develop' into feature/Image_Viewer 2021-02-07 17:58:48 +01:00
J-Jamet
4b73c45e65 Fix unzip issue 2021-02-07 15:52:20 +01:00
J-Jamet
ac248d8b73 Merge branch 'develop' into feature/Encrypt_Temp_Binaries 2021-02-07 15:24:28 +01:00
J-Jamet
726b0d0fa3 Merge branch 'feature/Refactor_Main_Credential' into develop 2021-02-07 15:23:42 +01:00
J-Jamet
2d40164549 Remove TODO 2021-02-07 14:52:43 +01:00
J-Jamet
5203152f78 Better main credential factorization 2021-02-07 14:33:36 +01:00
J-Jamet
b064bb74cd Better main credential factorization 2021-02-07 14:26:57 +01:00
J-Jamet
843d8e8e77 Refactor Main Credential object 2021-02-07 14:06:44 +01:00
J-Jamet
c5f95b243d Merge branch 'develop' into feature/Encrypt_Temp_Binaries 2021-02-07 13:02:16 +01:00
J-Jamet
06f1f4c8ad Fix new database version 2021-02-07 13:02:01 +01:00
J-Jamet
9847f834c2 Check length during binary unit tests 2021-02-07 10:51:56 +01:00
J-Jamet
b744d58e6c Better upload method 2021-02-07 10:51:39 +01:00
J-Jamet
c95358b344 Fix binary padding 2021-02-06 23:15:49 +01:00
J-Jamet
2f15d6c9f2 Show buffer copy 2021-02-06 20:57:28 +01:00
J-Jamet
d13aa047d5 Fix length and better streams implementation 2021-02-06 20:24:07 +01:00
J-Jamet
7590d18c67 Better copy methods 2021-02-06 17:23:03 +01:00
Michalis
a689116c97 Translated using Weblate (Greek)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-02-06 00:42:03 +01:00
J-Jamet
3bf7459f05 Fix binary stream and change cipher to be faster 2021-02-05 15:39:34 +01:00
J-Jamet
c70faaedd1 Rename BinaryAttachment to BinaryAttachmentTest 2021-02-05 13:44:30 +01:00
J-Jamet
cb2417fbe4 Better unit test for attachment 2021-02-05 13:43:36 +01:00
J-Jamet
65dd996f2e Merge branch 'feature/Unit_Test_Binary' into feature/Encrypt_Temp_Binaries 2021-02-05 13:14:26 +01:00
J-Jamet
01790a6f31 Add binary unit test 2021-02-05 13:14:05 +01:00
Jakub Fabijan
8b84bb893d Translated using Weblate (Esperanto)
Currently translated at 28.7% (147 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eo/
2021-02-01 22:40:31 +01:00
Óscar Fernández Díaz
8cb99847c5 Translated using Weblate (Spanish)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-02-01 22:40:28 +01:00
zeritti
b3e01277d4 Translated using Weblate (Czech)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-02-01 22:40:27 +01:00
Jakub Fabijan
b10d407659 Added translation using Weblate (Esperanto) 2021-02-01 01:34:06 +01:00
WaldiS
b7c5a5d238 Translated using Weblate (Polish)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-01-29 15:32:14 +01:00
J-Jamet
90d1ce63e8 Encrypt and decrypt temp files 2021-01-28 14:23:44 +01:00
Allan Nordhøy
9e30b4e5f7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.7% (357 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2021-01-28 05:32:14 +01:00
J-Jamet
e541a8c629 Merge branch 'develop' into feature/Encrypt_Temp_Binaries 2021-01-26 18:36:39 +01:00
J-Jamet
0afe25c922 Scroll and better UI in entry edition screen #876 2021-01-26 18:22:00 +01:00
J-Jamet
bca133430f Transform exceptions to be sure #877 2021-01-26 14:41:57 +01:00
J-Jamet
9c925518a7 Add toast if reload error #877 2021-01-26 14:25:56 +01:00
J-Jamet
18e79b99e7 Move notifications package to services 2021-01-26 12:34:12 +01:00
J-Jamet
4e02846df9 Update CHANGELOG 2021-01-26 12:15:48 +01:00
J-Jamet
2268b78bba Allow Emoji #796 2021-01-26 12:14:24 +01:00
J-Jamet
ea8acd0677 Upgrade version and CHANGELOG 2021-01-26 09:16:41 +01:00
J-Jamet
9e931dd03f Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2021-01-26 09:12:34 +01:00
J-Jamet
19e3aabca4 Try to fix TOTP plugin #878 2021-01-26 09:11:29 +01:00
Milo Ivir
ae697d82d5 Translated using Weblate (Croatian)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-01-25 19:32:14 +01:00
Oğuz Ersen
33382273c3 Translated using Weblate (Turkish)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-01-24 04:24:11 +01:00
Eric
7d8466d77a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-01-24 04:24:10 +01:00
Ihor Hordiichuk
5ca08a00d2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-01-24 04:24:10 +01:00
solokot
df3942697e Translated using Weblate (Russian)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-01-24 04:24:10 +01:00
Oliver Cervera
5700ca5bcf Translated using Weblate (Italian)
Currently translated at 99.2% (508 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-01-24 04:24:09 +01:00
Kunzisoft
54b2419d64 Translated using Weblate (French)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-01-24 04:24:09 +01:00
Retrial
d8b1c94b78 Translated using Weblate (Greek)
Currently translated at 100.0% (512 of 512 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-01-24 04:24:09 +01:00
J-Jamet
2d1ffc23b9 Merge tag '2.9.12' into develop
2.9.12
2021-01-23 13:44:39 +01:00
J-Jamet
da761614bd Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2021-01-22 18:03:36 +01:00
J-Jamet
9eb66face5 Fix reload exception 2021-01-20 12:46:08 +01:00
J-Jamet
45da17adb8 Encrypt temp binaries 2021-01-04 16:56:57 +01:00
J-Jamet
58d10672ea First implementation 2020-12-02 09:28:44 +01:00
236 changed files with 7992 additions and 3112 deletions

View File

@@ -1,3 +1,21 @@
KeePassDX(2.9.14)
* Add custom icons #96
* Dark Themes #532 #714
* Fix binary deduplication #715
* Fix IconId #901
* Resize image stream dynamically to prevent slowdown #919
* Small changes #795 #900 #903 #909 #914
KeePassDX(2.9.13)
* Binary image viewer #473 #749
* Fix TOTP plugin settings #878
* Allow Emoji #796
* Scroll and better UI in entry edition screen #876
* Better UI #876
* Fix themes and add Purple Dark #889
* Allow OTP with many padding #585
* Add notes in groups #734
KeePassDX(2.9.12) KeePassDX(2.9.12)
* Fix OTP token type #863 * Fix OTP token type #863
* Fix auto open biometric prompt #862 * Fix auto open biometric prompt #862

View File

@@ -1,23 +1,22 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.3' buildToolsVersion "30.0.3"
ndkVersion '21.3.6528147' ndkVersion "21.4.7075529"
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 30 targetSdkVersion 30
versionCode = 56 versionCode = 65
versionName = "2.9.12" versionName = "2.9.14"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}" buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ] manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
@@ -51,7 +50,11 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"libre\"" buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true" buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "false" buildConfigField "boolean", "CLOSED_STORE", "false"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}" buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
} }
pro { pro {
@@ -70,7 +73,13 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"free\"" buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}" buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Blue\"," +
"\"KeepassDXStyle_Blue_Night\"," +
"\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ] manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
} }
@@ -82,6 +91,10 @@ android {
free.res.srcDir 'src/free/res' free.res.srcDir 'src/free/res'
} }
testOptions {
unitTests.includeAndroidResources = true
}
compileOptions { compileOptions {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@@ -100,18 +113,19 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0-rc01' implementation 'androidx.biometric:biometric:1.1.0-rc01'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.fragment:fragment-ktx:1.2.5'
// WARNING: To upgrade with style, bug in edit text // WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.1.0'
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill // Autofill
implementation "androidx.autofill:autofill:1.1.0-rc01" implementation "androidx.autofill:autofill:1.1.0"
// Crypto // Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
// Time // Time
@@ -120,14 +134,14 @@ dependencies {
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
// Apache Commons Collections // Apache Commons
implementation 'commons-collections:commons-collections:3.2.2' implementation 'commons-io:commons-io:2.8.0'
// Apache Commons Codec
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
// Icon pack // Icon pack
implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')
// Tests // Tests
androidTestImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -0,0 +1,133 @@
Basic Latin
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
Latin-1 Supplement
¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ­ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ
Latin Extended-A
Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ
Latin Extended-B
ƀ Ɓ Ƃ ƃ Ƅ ƅ Ɔ Ƈ ƈ Ɖ Ɗ Ƌ ƌ ƍ Ǝ Ə Ɛ Ƒ ƒ Ɠ Ɣ ƕ Ɩ Ɨ Ƙ ƙ ƚ ƛ Ɯ Ɲ ƞ Ɵ Ơ ơ Ƣ ƣ Ƥ ƥ Ʀ Ƨ ƨ Ʃ ƪ ƫ Ƭ ƭ Ʈ Ư ư Ʊ Ʋ Ƴ ƴ Ƶ ƶ Ʒ Ƹ ƹ ƺ ƻ Ƽ ƽ ƾ ƿ ǀ ǁ ǂ ǃ DŽ Dž dž LJ Lj lj NJ Nj nj Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ Ǘ ǘ Ǚ ǚ Ǜ ǜ ǝ Ǟ ǟ Ǡ ǡ Ǣ ǣ Ǥ ǥ Ǧ ǧ Ǩ ǩ Ǫ ǫ Ǭ ǭ Ǯ ǯ ǰ DZ Dz dz Ǵ ǵ Ǻ ǻ Ǽ ǽ Ǿ ǿ Ȁ ȁ Ȃ ȃ ...
IPA Extensions
ɐ ɑ ɒ ɓ ɔ ɕ ɖ ɗ ɘ ə ɚ ɛ ɜ ɝ ɞ ɟ ɠ ɡ ɢ ɣ ɤ ɥ ɦ ɧ ɨ ɩ ɪ ɫ ɬ ɭ ɮ ɯ ɰ ɱ ɲ ɳ ɴ ɵ ɶ ɷ ɸ ɹ ɺ ɻ ɼ ɽ ɾ ɿ ʀ ʁ ʂ ʃ ʄ ʅ ʆ ʇ ʈ ʉ ʊ ʋ ʌ ʍ ʎ ʏ ʐ ʑ ʒ ʓ ʔ ʕ ʖ ʗ ʘ ʙ ʚ ʛ ʜ ʝ ʞ ʟ ʠ ʡ ʢ ʣ ʤ ʥ ʦ ʧ ʨ
Spacing Modifier Letters
ʰ ʱ ʲ ʳ ʴ ʵ ʶ ʷ ʸ ʹ ʺ ʻ ʼ ʽ ʾ ʿ ˀ ˁ ˂ ˃ ˄ ˅ ˆ ˇ ˈ ˉ ˊ ˋ ˌ ˍ ˎ ˏ ː ˑ ˒ ˓ ˔ ˕ ˖ ˗ ˘ ˙ ˚ ˛ ˜ ˝ ˞ ˠ ˡ ˢ ˣ ˤ ˥ ˦ ˧ ˨ ˩
Combining Diacritical Marks
̀ ́ ̂ ̃ ̄ ̅ ̆ ̇ ̈ ̉ ̊ ̋ ̌ ̍ ̎ ̏ ̐ ̑ ̒ ̓ ̔ ̕ ̖ ̗ ̘ ̙ ̚ ̛ ̜ ̝ ̞ ̟ ̠ ̡ ̢ ̣ ̤ ̥ ̦ ̧ ̨ ̩ ̪ ̫ ̬ ̭ ̮ ̯ ̰ ̱ ̲ ̳ ̴ ̵ ̶ ̷ ̸ ̹ ̺ ̻ ̼ ̽ ̾ ̿ ̀ ́ ͂ ̓ ̈́ ͅ ͠ ͡
Greek
ʹ ͵ ͺ ; ΄ ΅ Ά · Έ Ή Ί Ό Ύ Ώ ΐ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή ί ΰ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω ϊ ϋ ό ύ ώ ϐ ϑ ϒ ϓ ϔ ϕ ϖ Ϛ Ϝ Ϟ Ϡ Ϣ ϣ Ϥ ϥ Ϧ ϧ Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ
Cyrillic
Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ё ђ ѓ є ѕ і ї ј љ њ ћ ќ ў џ Ѡ ѡ Ѣ ѣ Ѥ ѥ Ѧ ѧ Ѩ ѩ Ѫ ѫ Ѭ ѭ Ѯ ѯ Ѱ ѱ Ѳ ѳ Ѵ ѵ Ѷ ѷ Ѹ ѹ Ѻ ѻ Ѽ ѽ Ѿ ѿ Ҁ ҁ ҂ ҃ ...
Armenian
Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք Օ Ֆ ՙ ՚ ՛ ՜ ՝ ՞ ՟ ա բ գ դ ե զ է ը թ ժ ի լ խ ծ կ հ ձ ղ ճ մ յ ն շ ո չ պ ջ ռ ս վ տ ր ց ւ փ ք օ ֆ և ։
Hebrew
֑ ֒ ֓ ֔ ֕ ֖ ֗ ֘ ֙ ֚ ֛ ֜ ֝ ֞ ֟ ֠ ֡ ֣ ֤ ֥ ֦ ֧ ֨ ֩ ֪ ֫ ֬ ֭ ֮ ֯ ְ ֱ ֲ ֳ ִ ֵ ֶ ַ ָ ֹ ֻ ּ ֽ ־ ֿ ׀ ׁ ׂ ׃ ׄ א ב ג ד ה ו ז ח ט י ך כ ל ם מ ן נ ס ע ף פ ץ צ ק ר ש ת װ ױ ײ ׳ ״
Arabic
، ؛ ؟ ء آ أ ؤ إ ئ ا ب ة ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ـ ف ق ك ل م ن ه و ى ي ً ٌ ٍ َ ُ ِ ّ ْ ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٪ ٫ ٬ ٭ ٰ ٱ ٲ ٳ ٴ ٵ ٶ ٷ ٸ ٹ ٺ ٻ ټ ٽ پ ٿ ڀ ځ ڂ ڃ ڄ څ چ ڇ ڈ ډ ڊ ڋ ڌ ڍ ڎ ڏ ڐ ڑ ڒ ړ ڔ ڕ ږ ڗ ژ ڙ ښ ڛ ڜ ڝ ڞ ڟ ڠ ڡ ڢ ڣ ڤ ڥ ڦ ڧ ڨ ک ڪ ګ ڬ ڭ ڮ گ ڰ ڱ ...
Devanagari
ँ ं अ आ इ ई उ ऊ ऋ ऌ ऍ ऎ ए ऐ ऑ ऒ ओ औ क ख ग घ ङ च छ ज झ ञ ट ठ ड ढ ण त थ द ध न ऩ प फ ब भ म य र ऱ ल ळ ऴ व श ष स ह ़ ऽ ा ि ी ु ू ृ ॄ ॅ ॆ े ै ॉ ॊ ो ौ ् ॐ ॑ ॒ ॓ ॔ क़ ख़ ग़ ज़ ड़ ढ़ फ़ य़ ॠ ॡ ॢ ॣ । ॥ १ २ ३ ४ ५ ६ ७ ८ ९ ॰
Bengali
ঁ ং ঃ অ আ ই ঈ উ ঊ ঋ ঌ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ় া ি ী ু ূ ৃ ৄ ে ৈ ো ৌ ্ ৗ ড় ঢ় য় ৠ ৡ ৢ ৣ ১ ২ ৩ ৫ ৬ ৮ ৯ ৰ ৱ ৲ ৳ ৴ ৵ ৶ ৷ ৸ ৹ ৺
Gurmukhi
ਂ ਅ ਆ ਇ ਈ ਉ ਊ ਏ ਐ ਓ ਔ ਕ ਖ ਗ ਘ ਙ ਚ ਛ ਜ ਝ ਞ ਟ ਠ ਡ ਢ ਣ ਤ ਥ ਦ ਧ ਨ ਪ ਫ ਬ ਭ ਮ ਯ ਰ ਲ ਲ਼ ਵ ਸ਼ ਸ ਹ ਼ ਾ ਿ ੀ ੁ ੂ ੇ ੈ ੋ ੌ ੍ ਖ਼ ਗ਼ ਜ਼ ੜ ਫ਼ ੨ ੩ ੫ ੬ ੭ ੮ ੯ ੰ ੱ ੲ ੳ ੴ
Gujarati
ઁ ં અ આ ઇ ઈ ઉ ઊ ઋ ઍ એ ઐ ઑ ઓ ઔ ક ખ ગ ઘ ઙ ચ છ જ ઝ ઞ ટ ઠ ડ ઢ ણ ત થ દ ધ ન પ ફ બ ભ મ ય ર લ ળ વ શ ષ સ હ ઼ ઽ ા િ ી ુ ૂ ૃ ૄ ૅ ે ૈ ૉ ો ૌ ્ ૐ ૠ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯
Oriya
ଁ ଂ ଅ ଆ ଇ ଈ ଉ ଊ ଋ ଌ ଏ ଐ ଓ ଔ କ ଖ ଗ ଘ ଙ ଚ ଛ ଜ ଝ ଞ ଟ ଡ ଢ ଣ ତ ଥ ଦ ଧ ନ ପ ଫ ବ ଭ ମ ଯ ର ଲ ଳ ଶ ଷ ସ ହ ଼ ଽ ା ି ୀ ୁ ୂ ୃ େ ୈ ୋ ୌ ୍ ୖ ୗ ଡ଼ ଢ଼ ୟ ୠ ୡ ୩ ୪ ୫ ୬ ୭ ୮ ୯ ୰
Tamil
ஂ ஃ அ ஆ இ ஈ உ ஊ எ ஏ ஐ ஒ ஓ ஔ க ங ச ஜ ஞ ட ண த ந ன ப ம ய ர ற ல ள ழ வ ஷ ஸ ஹ ா ி ீ ு ூ ெ ே ை ொ ோ ௌ ் ௗ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ ௰ ௱ ௲
Telugu
ః అ ఆ ఇ ఈ ఉ ఊ ఋ ఌ ఎ ఏ ఐ ఒ ఓ ఔ క ఖ గ ఘ ఙ చ ఛ జ ఝ ఞ ట ఠ డ ఢ ణ త థ ద ధ న ప ఫ బ భ మ య ర ఱ ల ళ వ శ ష స హ ా ి ీ ు ూ ృ ౄ ె ే ై ొ ో ౌ ్ ౕ ౖ ౠ ౡ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯
Kannada
ಃ ಅ ಆ ಇ ಈ ಉ ಊ ಋ ಌ ಎ ಏ ಐ ಒ ಓ ಔ ಕ ಖ ಗ ಘ ಙ ಚ ಛ ಜ ಝ ಞ ಟ ಠ ಡ ಢ ಣ ತ ಥ ದ ಧ ನ ಪ ಫ ಬ ಭ ಮ ಯ ರ ಱ ಲ ಳ ವ ಶ ಷ ಸ ಹ ಾ ಿ ೀ ು ೂ ೃ ೄ ೆ ೇ ೈ ೊ ೋ ೌ ್ ೕ ೖ ೞ ೠ ೡ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯
Malayalam
ഃ അ ആ ഇ ഈ ഉ ഊ ഋ ഌ എ ഏ ഐ ഒ ഓ ഔ ക ഖ ഗ ഘ ങ ച ഛ ജ ഝ ഞ ട ഡ ഢ ണ ത ഥ ദ ധ ന പ ഫ ബ ഭ മ യ ര റ ല ള ഴ വ ശ ഷ സ ഹ ാ ി ീ ു ൂ ൃ െ േ ൈ ൊ ോ ൌ ് ൗ ൠ ൡ ൧ ൨ ൩ ൪ ൫ ൬ ൮ ൯
Thai
ก ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ ญ ฎ ฏ ฐ ฑ ฒ ณ ด ต ถ ท ธ น บ ป ผ ฝ พ ฟ ภ ม ย ร ฤ ล ฦ ว ศ ษ ส ห ฬ อ ฮ ฯ ะ ั า ำ ิ ี ึ ื ุ ู ฺ ฿ เ แ โ ใ ไ ๅ ๆ ็ ่ ้ ๊ ๋ ์ ํ ๎ ๏ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๚ ๛
Lao
ກ ຂ ຄ ງ ຈ ຊ ຍ ດ ຕ ຖ ທ ນ ບ ປ ຜ ຝ ພ ຟ ມ ຢ ຣ ລ ວ ສ ຫ ອ ຮ ຯ ະ ັ າ ຳ ິ ີ ຶ ື ຸ ູ ົ ຼ ຽ ເ ແ ໂ ໃ ໄ ໆ ່ ້ ໊ ໋ ໌ ໍ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ ໜ ໝ
Tibetan
ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊ ་ ༌ ། ༎ ༏ ༐ ༑ ༒ ༓ ༔ ༕ ༖ ༗ ༘ ༙ ༚ ༛ ༜ ༝ ༞ ༟ ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ ༪ ༫ ༬ ༭ ༮ ༯ ༰ ༱ ༲ ༳ ༴ ༵ ༶ ༷ ༸ ༹ ༺ ༻ ༼ ༽ ༾ ༿ ཀ ཁ ག གྷ ང ཅ ཆ ཇ ཉ ཊ ཋ ཌ ཌྷ ཎ ཏ ཐ ད དྷ ན པ ཕ བ བྷ མ ཙ ཚ ཛ ཛྷ ཝ ཞ ཟ འ ཡ ར ལ ཤ ཥ ས ཧ ཨ ཀྵ ཱ ི ཱི ུ ཱུ ྲྀ ཷ ླྀ ཹ ེ ཻ ོ ཽ ཾ ཿ ྀ ཱྀ ྂ ྃ ྄ ྅ ྆ ྇ ...
Georgian
Ⴀ Ⴁ Ⴂ Ⴃ Ⴄ Ⴅ Ⴆ Ⴇ Ⴈ Ⴉ Ⴊ Ⴋ Ⴌ Ⴍ Ⴎ Ⴏ Ⴐ Ⴑ Ⴒ Ⴓ Ⴔ Ⴕ Ⴖ Ⴗ Ⴘ Ⴙ Ⴚ Ⴛ Ⴜ Ⴝ Ⴞ Ⴟ Ⴠ Ⴡ Ⴢ Ⴣ Ⴤ Ⴥ ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ ჟ რ ს ტ უ ფ ქ ღ შ ჩ ც ძ წ ჭ ხ ჯ ჰ ჱ ჲ ჳ ჴ ჵ ჶ ჻
Hangul Jamo
ᄀ ᄁ ᄂ ᄃ ᄄ ᄅ ᄆ ᄇ ᄈ ᄉ ᄊ ᄋ ᄌ ᄍ ᄎ ᄏ ᄐ ᄑ ᄒ ᄓ ᄔ ᄕ ᄖ ᄗ ᄘ ᄙ ᄚ ᄛ ᄜ ᄝ ᄞ ᄟ ᄠ ᄡ ᄢ ᄣ ᄤ ᄥ ᄦ ᄧ ᄨ ᄩ ᄪ ᄫ ᄬ ᄭ ᄮ ᄯ ᄰ ᄱ ᄲ ᄳ ᄴ ᄵ ᄶ ᄷ ᄸ ᄹ ᄺ ᄻ ᄼ ᄽ ᄾ ᄿ ᅀ ᅁ ᅂ ᅃ ᅄ ᅅ ᅆ ᅇ ᅈ ᅉ ᅊ ᅋ ᅌ ᅍ ᅎ ᅏ ᅐ ᅑ ᅒ ᅓ ᅔ ᅕ ᅖ ᅗ ᅘ ᅙ ᅡ ᅢ ᅣ ᅤ ᅥ ᅦ ᅧ ᅨ ᅩ ᅪ ᅫ ᅬ ᅭ ᅮ ᅯ ᅰ ᅱ ᅲ ᅳ ᅴ ᅵ ᅶ ᅷ ᅸ ᅹ ᅺ ᅻ ᅼ ᅽ ᅾ ᅿ ᆀ ᆁ ᆂ ᆃ ᆄ ...
Latin Extended Additional
Ḁ ḁ Ḃ ḃ Ḅ ḅ Ḇ ḇ Ḉ ḉ Ḋ ḋ Ḍ ḍ Ḏ ḏ Ḑ ḑ Ḓ ḓ Ḕ ḕ Ḗ ḗ Ḙ ḙ Ḛ ḛ Ḝ ḝ Ḟ ḟ Ḡ ḡ Ḣ ḣ Ḥ ḥ Ḧ ḧ Ḩ ḩ Ḫ ḫ Ḭ ḭ Ḯ ḯ Ḱ ḱ Ḳ ḳ Ḵ ḵ Ḷ ḷ Ḹ ḹ Ḻ ḻ Ḽ ḽ Ḿ ḿ Ṁ ṁ Ṃ ṃ Ṅ ṅ Ṇ ṇ Ṉ ṉ Ṋ ṋ Ṍ ṍ Ṏ ṏ Ṑ ṑ Ṓ ṓ Ṕ ṕ Ṗ ṗ Ṙ ṙ Ṛ ṛ Ṝ ṝ Ṟ ṟ Ṡ ṡ Ṣ ṣ Ṥ ṥ Ṧ ṧ Ṩ ṩ Ṫ ṫ Ṭ ṭ Ṯ ṯ Ṱ ṱ Ṳ ṳ Ṵ ṵ Ṷ ṷ Ṹ ṹ Ṻ ṻ Ṽ ṽ Ṿ ṿ ...
Greek Extended
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ...
General Punctuation
  — ― ‖ ‗ “ ” „ ‟ † ‡ • ‣ ‥ … ‧ ‰ ‱ ″ ‴ ‶ ‷ ‸ ※ ‼ ‽ ‾ ‿ ⁀ ⁅ ⁆
Superscripts and Subscripts
⁰ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎
Currency Symbols
₠ ₡ ₢ ₣ ₤ ₥ ₦ ₧ ₨ ₩ ₪ ₫
Combining Marks for Symbols
⃐ ⃑ ⃒ ⃓ ⃔ ⃕ ⃖ ⃗ ⃘ ⃙ ⃚ ⃛ ⃜ ⃝ ⃞ ⃟ ⃠ ⃡
Letterlike Symbols
℀ ℁ ℃ ℄ ℅ ℆ ℇ ℈ ℉ № ℗ ℘ ℞ ℟ ℠ ℡ ™ ℣ ℥ Ω ℧ ℵ ℶ ℷ ℸ
Number Forms
⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ⅟ Ⅱ Ⅲ Ⅳ Ⅵ Ⅶ Ⅷ Ⅸ Ⅺ Ⅻ ⅱ ⅲ ⅳ ⅵ ⅶ ⅷ ⅸ ⅺ ⅻ ⅿ ↀ ↁ ↂ
Arrows
← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟ ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯ ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿ ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏ ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟ ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪
Mathematical Operators
∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏ ∐ ∑ ∓ ∔ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟ ∠ ∡ ∢ ∤ ∥ ∦ ∧ ∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ∴ ∵ ∷ ∸ ∹ ∺ ∻ ∽ ∾ ∿ ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ...
Miscellaneous Technical
⌀ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏ ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟ ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯ ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿ ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏ ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟ ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯ ⍰ ⍱ ⍲ ⍵ ⍶ ⍷ ⍸ ⍹
Control Pictures
␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ ␠ ␡ ␢ ␣ ␤
Optical Character Recognition
⑀ ⑁ ⑂ ⑃ ⑄ ⑅ ⑆ ⑇ ⑈ ⑉ ⑊
Enclosed Alphanumerics
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ...
Box Drawing
─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
Block Elements
▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕
Geometric Shapes
■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯
Miscellaneous Symbols
☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯
Dingbats
✁ ✂ ✃ ✄ ✆ ✇ ✈ ✉ ✌ ✍ ✎ ✏ ✐ ✑ ✒ ✓ ✔ ✕ ✖ ✗ ✘ ✙ ✚ ✛ ✜ ✝ ✞ ✟ ✠ ✡ ✢ ✣ ✤ ✥ ✦ ✧ ✩ ✪ ✫ ✬ ✭ ✮ ✯ ✰ ✱ ✲ ✳ ✴ ✵ ✶ ✷ ✸ ✹ ✺ ✻ ✼ ✽ ✾ ✿ ❀ ❁ ❂ ❃ ❄ ❅ ❆ ❇ ❈ ❉ ❊ ❋ ❍ ❏ ❐ ❑ ❒ ❖ ❘ ❙ ❚ ❛ ❜ ❝ ❞ ❡ ❢ ❣ ❤ ❥ ❦ ❧ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑ ➒ ➓ ➔ ➘ ➙ ➚ ➛ ➜ ➝ ...
CJK Symbols and Punctuation
  、 。 〃 〄 々 〆 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〴 〵 〶 〷 〿
Hiragana
ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゙ ゚ ゛ ゜ ゝ ゞ
Katakana
ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ ヷ ヸ ヹ ヺ ・ ー ヽ ヾ
Bopomofo
ㄅ ㄆ ㄇ ㄈ ㄉ ㄊ ㄋ ㄌ ㄍ ㄎ ㄏ ㄐ ㄑ ㄒ ㄓ ㄔ ㄕ ㄖ ㄗ ㄘ ㄙ ㄚ ㄛ ㄜ ㄝ ㄞ ㄟ ㄠ ㄡ ㄢ ㄣ ㄤ ㄥ ㄦ ㄧ ㄨ ㄩ ㄪ ㄫ ㄬ
Hangul Compatibility Jamo
ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
Kanbun
㆐ ㆑ ㆒ ㆓ ㆔ ㆕ ㆖ ㆗ ㆘ ㆙ ㆚ ㆛ ㆜ ㆝ ㆞ ㆟
Enclosed CJK Letters and Months
㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛ ㈜ ㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㉿ ㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ...
CJK Compatibility
㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿ ㎀ ㎁ ㎂ ㎃ ...
CJK Unified Ideographs
一 丁 丂 七 丄 丅 丆 万 丈 三 上 下 丌 不 与 丏 丐 丑 丒 专 且 丕 世 丗 丘 丙 业 丛 东 丝 丞 丟 丠 両 丢 丣 两 严 並 丧 丨 丩 个 丫 丬 中 丮 丯 丰 丱 串 丳 临 丵 丷 丸 丹 为 主 丼 丽 举 丿 乀 乁 乂 乃 乄 久 乆 乇 么 义 乊 之 乌 乍 乎 乏 乐 乑 乒 乓 乔 乕 乖 乗 乘 乙 乚 乛 乜 九 乞 也 习 乡 乢 乣 乤 乥 书 乧 乨 乩 乪 乫 乬 乭 乮 乯 买 乱 乲 乳 乴 乵 乶 乷 乸 乹 乺 乻 乼 乽 乾 乿 ...
Hangul Syllables
가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 감 갑 값 갓 갔 강 갖 갗 갘 같 갚 갛 개 객 갞 갟 갠 갡 갢 갣 갤 갥 갦 갧 갨 갩 갪 갫 갬 갭 갮 갯 갰 갱 갲 갳 갴 갵 갶 갷 갸 갹 갺 갻 갼 갽 갾 갿 걀 걁 걂 걃 걄 걅 걆 걇 걈 걉 걊 걋 걌 걍 걎 걏 걐 걑 걒 걓 걔 걕 걖 걗 걘 걙 걚 걛 걜 걝 걞 걟 걠 걡 걢 걣 걤 걥 걦 걧 걨 걩 걪 걫 걬 걭 걮 걯 거 걱 걲 걳 건 걵 걶 걷 걸 걹 걺 걻 걼 걽 걾 걿 ...
Private Use
                                                                                                                                ...
CJK Compatibility Ideographs
豈 更 車 賈 滑 串 句 龜 龜 契 金 喇 奈 懶 癩 羅 蘿 螺 裸 邏 樂 洛 烙 珞 落 酪 駱 亂 卵 欄 爛 蘭 鸞 嵐 濫 藍 襤 拉 臘 蠟 廊 朗 浪 狼 郎 來 冷 勞 擄 櫓 爐 盧 老 蘆 虜 路 露 魯 鷺 碌 祿 綠 菉 錄 鹿 論 壟 弄 籠 聾 牢 磊 賂 雷 壘 屢 樓 淚 漏 累 縷 陋 勒 肋 凜 凌 稜 綾 菱 陵 讀 拏 樂 諾 丹 寧 怒 率 異 北 磻 便 復 不 泌 數 索 參 塞 省 葉 說 殺 辰 沈 拾 若 掠 略 亮 兩 凉 梁 糧 良 諒 量 勵 ...
Alphabetic Presentation Forms
ff fi fl ffi ffl ſt st ﬓ ﬔ ﬕ ﬖ ﬗ ﬞ ײַ ﬠ ﬡ ﬢ ﬣ ﬤ ﬥ ﬦ ﬧ ﬨ ﬩ שׁ שׂ שּׁ שּׂ אַ אָ אּ בּ גּ דּ הּ וּ זּ טּ יּ ךּ כּ לּ מּ נּ סּ ףּ פּ צּ קּ רּ שּ תּ וֹ בֿ כֿ פֿ ﭏ
Arabic Presentation Forms-A
ﭐ ﭑ ﭒ ﭓ ﭔ ﭕ ﭖ ﭗ ﭘ ﭙ ﭚ ﭛ ﭜ ﭝ ﭞ ﭟ ﭠ ﭡ ﭢ ﭣ ﭤ ﭥ ﭦ ﭧ ﭨ ﭩ ﭪ ﭫ ﭬ ﭭ ﭮ ﭯ ﭰ ﭱ ﭲ ﭳ ﭴ ﭵ ﭶ ﭷ ﭸ ﭹ ﭺ ﭻ ﭼ ﭽ ﭾ ﭿ ﮀ ﮁ ﮂ ﮃ ﮄ ﮅ ﮆ ﮇ ﮈ ﮉ ﮊ ﮋ ﮌ ﮍ ﮎ ﮏ ﮐ ﮑ ﮒ ﮓ ﮔ ﮕ ﮖ ﮗ ﮘ ﮙ ﮚ ﮛ ﮜ ﮝ ﮞ ﮟ ﮠ ﮡ ﮢ ﮣ ﮤ ﮥ ﮮ ﮯ ﮰ ﮱ ﯓ ﯔ ﯕ ﯖ ﯗ ﯘ ﯙ ﯚ ﯛ ﯜ ﯝ ﯞ ﯟ ﯠ ﯡ ﯢ ﯣ ﯤ ﯥ ﯦ ﯧ ﯨ ﯩ ﯪ ﯫ ﯬ ﯭ ﯮ ﯯ ﯰ ...
Combining Half Marks
︠ ︡ ︢ ︣
CJK Compatibility Forms
︱ ︲ ︳ ︴ ︵ ︶ ︷ ︸ ︹ ︺ ︻ ︼ ︽ ︾ ︿ ﹀ ﹁ ﹂ ﹃ ﹄ ﹉ ﹊ ﹋ ﹌
Small Form Variants
﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟ ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹩ ﹪ ﹫
Arabic Presentation Forms-B
ﹰ ﹱ ﹲ ﹴ ﹶ ﹷ ﹸ ﹹ ﹺ ﹻ ﹼ ﹽ ﹾ ﹿ ﺀ ﺁ ﺂ ﺃ ﺄ ﺅ ﺆ ﺇ ﺈ ﺉ ﺊ ﺋ ﺌ ﺏ ﺐ ﺑ ﺒ ﺓ ﺔ ﺕ ﺖ ﺗ ﺘ ﺙ ﺚ ﺛ ﺜ ﺝ ﺞ ﺟ ﺠ ﺡ ﺢ ﺣ ﺤ ﺥ ﺦ ﺧ ﺨ ﺩ ﺪ ﺫ ﺬ ﺭ ﺮ ﺯ ﺰ ﺱ ﺲ ﺳ ﺴ ﺵ ﺶ ﺷ ﺸ ﺹ ﺺ ﺻ ﺼ ﺽ ﺾ ﺿ ﻀ ﻁ ﻂ ﻃ ﻄ ﻅ ﻆ ﻇ ﻈ ﻉ ﻊ ﻋ ﻌ ﻍ ﻎ ﻏ ﻐ ﻑ ﻒ ﻓ ﻔ ﻕ ﻖ ﻗ ﻘ ﻙ ﻚ ﻛ ﻜ ﻝ ﻞ ﻟ ﻠ ﻡ ﻢ ﻣ ﻤ ﻥ ﻦ ﻧ ﻨ ﻭ ﻮ ﻯ ﻰ ﻱ ...
Halfwidth and Fullwidth Forms
_ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
Specials

Specials
<20>

View File

@@ -0,0 +1,160 @@
package com.kunzisoft.keepass.tests.stream
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryByte
import com.kunzisoft.keepass.database.element.database.BinaryFile
import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import junit.framework.TestCase.assertEquals
import org.junit.Test
import java.io.DataInputStream
import java.io.File
import java.io.InputStream
import kotlin.random.Random
class BinaryDataTest {
private val context: Context by lazy {
InstrumentationRegistry.getInstrumentation().context
}
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
private val loadedKey = Database.LoadedKey.generateNewCipherKey()
private fun saveBinary(asset: String, binaryData: BinaryFile) {
context.assets.open(asset).use { assetInputStream ->
binaryData.getOutputDataStream(loadedKey).use { binaryOutputStream ->
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
binaryOutputStream.write(buffer)
}
}
}
}
@Test
fun testSaveTextInCache() {
val binaryA = BinaryFile(fileA)
val binaryB = BinaryFile(fileB)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
assertEquals("Save text binary length failed.", binaryA.getSize(), binaryB.getSize())
assertEquals("Save text binary MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
}
@Test
fun testSaveImageInCache() {
val binaryA = BinaryFile(fileA)
val binaryB = BinaryFile(fileB)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
assertEquals("Save image binary length failed.", binaryA.getSize(), binaryB.getSize())
assertEquals("Save image binary failed.", binaryA.binaryHash(), binaryB.binaryHash())
}
@Test
fun testCompressText() {
val binaryA = BinaryFile(fileA)
val binaryB = BinaryFile(fileB)
val binaryC = BinaryFile(fileC)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
saveBinary(TEST_TEXT_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
binaryB.decompress(loadedKey)
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
}
@Test
fun testCompressImage() {
val binaryA = BinaryFile(fileA)
var binaryB = BinaryFile(fileB)
val binaryC = BinaryFile(fileC)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
saveBinary(TEST_IMAGE_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
binaryB = BinaryFile(fileB, true)
binaryB.decompress(loadedKey)
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
}
@Test
fun testCompressBytes() {
val byteArray = ByteArray(50)
Random.nextBytes(byteArray)
val binaryA = BinaryByte(byteArray)
val binaryB = BinaryByte(byteArray)
val binaryC = BinaryByte(byteArray)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
binaryB.decompress(loadedKey)
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
}
@Test
fun testReadText() {
val binaryA = BinaryFile(fileA)
saveBinary(TEST_TEXT_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
@Test
fun testReadImage() {
val binaryA = BinaryFile(fileA)
saveBinary(TEST_IMAGE_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
private fun streamAreEquals(inputStreamA: InputStream,
inputStreamB: InputStream): Boolean {
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
val dataInputStreamB = DataInputStream(inputStreamB)
try {
var len: Int
while (inputStreamA.read(bufferA).also { len = it } > 0) {
dataInputStreamB.readFully(bufferB, 0, len)
for (i in 0 until len) {
if (bufferA[i] != bufferB[i])
return false
}
}
return inputStreamB.read() < 0 // is the end of the second file also.
} catch (e: Exception) {
return false
}
finally {
inputStreamA.close()
inputStreamB.close()
}
}
companion object {
private const val TEST_FILE_CACHE_A = "testA"
private const val TEST_FILE_CACHE_B = "testB"
private const val TEST_FILE_CACHE_C = "testC"
private const val TEST_IMAGE_ASSET = "test_image.png"
private const val TEST_TEXT_ASSET = "test_text.txt"
}
}

View File

@@ -129,9 +129,15 @@
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntryActivity" android:name="com.kunzisoft.keepass.activities.EntryActivity"
android:configChanges="keyboardHidden" /> android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
android:configChanges="keyboardHidden" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity" android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize" />
<!-- About and Settings --> <!-- About and Settings -->
<activity <activity
android:name="com.kunzisoft.keepass.activities.AboutActivity" android:name="com.kunzisoft.keepass.activities.AboutActivity"
@@ -175,19 +181,19 @@
</activity> </activity>
<service <service
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService" android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<service <service
android:name="com.kunzisoft.keepass.notifications.AttachmentFileNotificationService" android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<service <service
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<service <service
android:name="com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService" android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<!-- Receiver for Autofill --> <!-- Receiver for Autofill -->
@@ -213,7 +219,7 @@
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />

File diff suppressed because it is too large Load Diff

View File

@@ -47,26 +47,22 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.createDocument
import com.kunzisoft.keepass.utils.onCreateDocumentResult
import com.kunzisoft.keepass.view.EntryContentsView import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
@@ -129,6 +125,7 @@ class EntryActivity : LockingActivity() {
historyView = findViewById(R.id.history_container) historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents) entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey)
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
@@ -156,10 +153,11 @@ class EntryActivity : LockingActivity() {
} }
ACTION_DATABASE_RELOAD_TASK -> { ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity // Close the current activity
this.showActionErrorIfNeeded(result)
finish() finish()
} }
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionErrorIfNeeded(result)
} }
} }
@@ -222,7 +220,9 @@ class EntryActivity : LockingActivity() {
registerProgressTask() registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
entryContentsView?.putAttachment(entryAttachmentState) if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
entryContentsView?.putAttachment(entryAttachmentState)
}
} }
} }
} }
@@ -241,7 +241,9 @@ class EntryActivity : LockingActivity() {
val entryInfo = entry.getEntryInfo(mDatabase) val entryInfo = entry.getEntryInfo(mDatabase)
// Assign title icon // Assign title icon
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor) titleIconView?.let { iconView ->
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
}
// Assign title text // Assign title text
val entryTitle = entryInfo.title val entryTitle = entryInfo.title
@@ -349,7 +351,7 @@ class EntryActivity : LockingActivity() {
// Assign dates // Assign dates
entryContentsView?.assignCreationDate(entryInfo.creationTime) entryContentsView?.assignCreationDate(entryInfo.creationTime)
entryContentsView?.assignModificationDate(entryInfo.modificationTime) entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime) entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
// Manage history // Manage history

View File

@@ -36,7 +36,6 @@ import android.widget.DatePicker
import android.widget.TimePicker import android.widget.TimePicker
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
@@ -44,6 +43,7 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
@@ -57,29 +57,28 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingLeft import com.kunzisoft.keepass.view.updateLockPaddingLeft
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class EntryEditActivity : LockingActivity(), class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
EntryCustomFieldDialogFragment.EntryCustomFieldListener, EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener, GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener, SetOTPDialogFragment.CreateOtpListener,
@@ -99,7 +98,7 @@ class EntryEditActivity : LockingActivity(),
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null private var scrollView: NestedScrollView? = null
private var entryEditFragment: EntryEditFragment? = null private var entryEditFragment: EntryEditFragment? = null
private var entryEditAddToolBar: Toolbar? = null private var entryEditAddToolBar: ToolbarAction? = null
private var validateButton: View? = null private var validateButton: View? = null
private var lockView: View? = null private var lockView: View? = null
@@ -107,7 +106,7 @@ class EntryEditActivity : LockingActivity(),
private var mSelectFileHelper: SelectFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false private var mAllowMultipleAttachments: Boolean = false
private var mTempAttachments = ArrayList<Attachment>() private var mTempAttachments = ArrayList<EntryAttachmentState>()
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -119,11 +118,12 @@ class EntryEditActivity : LockingActivity(),
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit) setContentView(R.layout.activity_entry_edit)
val toolbar = findViewById<Toolbar>(R.id.toolbar) // Bottom Bar
setSupportActionBar(toolbar) entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp) setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout) coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
@@ -172,10 +172,14 @@ class EntryEditActivity : LockingActivity(),
val parentIcon = mParent?.icon val parentIcon = mParent?.icon
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true) tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
// Set default icon // Set default icon
if (parentIcon != null if (parentIcon != null) {
&& parentIcon.iconId != IconImage.UNKNOWN_ID if (parentIcon.custom.isUnknown
&& parentIcon.iconId != IconImageStandard.FOLDER) { && parentIcon.standard.id != IconImageStandard.FOLDER_ID) {
tempEntryInfo?.icon = parentIcon tempEntryInfo?.icon = IconImage(parentIcon.standard)
}
if (!parentIcon.custom.isUnknown) {
tempEntryInfo?.icon = IconImage(parentIcon.custom)
}
} }
// Set default username // Set default username
tempEntryInfo?.username = mDatabase?.defaultUsername ?: "" tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
@@ -198,14 +202,14 @@ class EntryEditActivity : LockingActivity(),
// Build fragment to manage entry modification // Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment? entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) { if (entryEditFragment == null) {
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo) entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey)
} }
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG) .replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
.commit() .commit()
entryEditFragment?.apply { entryEditFragment?.apply {
drawFactory = mDatabase?.drawFactory drawFactory = mDatabase?.iconDrawableFactory
setOnDateClickListener = View.OnClickListener { setOnDateClickListener = {
expiryTime.date.let { expiresDate -> expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate) val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year val defaultYear = dateTime.year
@@ -219,8 +223,8 @@ class EntryEditActivity : LockingActivity(),
openPasswordGenerator() openPasswordGenerator()
} }
// Add listener to the icon // Add listener to the icon
setOnIconViewClickListener = View.OnClickListener { setOnIconViewClickListener = { iconImage ->
IconPickerDialogFragment.launch(this@EntryEditActivity) IconPickerActivity.launch(this@EntryEditActivity, iconImage)
} }
setOnRemoveAttachment = { attachment -> setOnRemoveAttachment = { attachment ->
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment) mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
@@ -236,51 +240,6 @@ class EntryEditActivity : LockingActivity(),
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
} }
// Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu.findItem(R.id.menu_add_attachment).isVisible = false
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> {
setupOTP()
true
}
else -> true
}
}
}
// To retrieve attachment // To retrieve attachment
mSelectFileHelper = SelectFileHelper(this) mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -338,10 +297,11 @@ class EntryEditActivity : LockingActivity(),
} }
ACTION_DATABASE_RELOAD_TASK -> { ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity // Close the current activity
this.showActionErrorIfNeeded(result)
finish() finish()
} }
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionErrorIfNeeded(result)
} }
} }
@@ -398,29 +358,27 @@ class EntryEditActivity : LockingActivity(),
when (entryAttachmentState.downloadState) { when (entryAttachmentState.downloadState) {
AttachmentState.START -> { AttachmentState.START -> {
entryEditFragment?.apply { entryEditFragment?.apply {
// When only one attachment is allowed
if (!mAllowMultipleAttachments) {
clearAttachments()
}
putAttachment(entryAttachmentState) putAttachment(entryAttachmentState)
// Scroll to the attachment position // Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) { getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt()) scrollView?.smoothScrollTo(0, it.toInt())
} }
} } // Add in temp list
mTempAttachments.add(entryAttachmentState)
} }
AttachmentState.IN_PROGRESS -> { AttachmentState.IN_PROGRESS -> {
entryEditFragment?.putAttachment(entryAttachmentState) entryEditFragment?.putAttachment(entryAttachmentState)
} }
AttachmentState.COMPLETE -> { AttachmentState.COMPLETE -> {
entryEditFragment?.apply { entryEditFragment?.putAttachment(entryAttachmentState) {
putAttachment(entryAttachmentState) entryEditFragment?.getAttachmentViewPosition(entryAttachmentState) {
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt()) scrollView?.smoothScrollTo(0, it.toInt())
} }
} }
} }
AttachmentState.CANCELED -> {
entryEditFragment?.removeAttachment(entryAttachmentState)
}
AttachmentState.ERROR -> { AttachmentState.ERROR -> {
entryEditFragment?.removeAttachment(entryAttachmentState) entryEditFragment?.removeAttachment(entryAttachmentState)
coordinatorLayout?.let { coordinatorLayout?.let {
@@ -516,16 +474,18 @@ class EntryEditActivity : LockingActivity(),
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) { private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
if (attachmentToUploadUri != null && attachment != null) { if (attachmentToUploadUri != null && attachment != null) {
// When only one attachment is allowed
if (!mAllowMultipleAttachments) {
entryEditFragment?.clearAttachments()
}
// Start uploading in service // Start uploading in service
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment) mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
// Add in temp list
mTempAttachments.add(attachment)
} }
} }
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) { private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
val compression = mDatabase?.compressionForNewEntry() ?: false val compression = mDatabase?.compressionForNewEntry() ?: false
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment -> mDatabase?.buildNewBinaryAttachment(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment) val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment // Ask to replace the current attachment
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) || if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
@@ -541,9 +501,12 @@ class EntryEditActivity : LockingActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
entryEditFragment?.icon = icon
}
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri -> mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri -> uri?.let { attachmentToUploadUri ->
// TODO Async to get the name
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile -> UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName -> documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) { if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
@@ -572,6 +535,7 @@ class EntryEditActivity : LockingActivity(),
* Saves the new entry or update an existing entry in the database * Saves the new entry or update an existing entry in the database
*/ */
private fun saveEntry() { private fun saveEntry() {
mAttachmentFileBinderManager?.stopUploadAllAttachments()
// Get the temp entry // Get the temp entry
entryEditFragment?.getEntryInfo()?.let { newEntryInfo -> entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
@@ -583,14 +547,34 @@ class EntryEditActivity : LockingActivity(),
Entry(mEntry!!) Entry(mEntry!!)
}?.let { newEntry -> }?.let { newEntry ->
// Do not save entry in upload progression
mTempAttachments.forEach { attachmentState ->
if (attachmentState.streamDirection == StreamDirection.UPLOAD) {
when (attachmentState.downloadState) {
AttachmentState.START,
AttachmentState.IN_PROGRESS,
AttachmentState.CANCELED,
AttachmentState.ERROR -> {
// Remove attachment not finished from info
newEntryInfo.attachments = newEntryInfo.attachments.toMutableList().apply {
remove(attachmentState.attachment)
}
}
else -> {
}
}
}
}
// Build info // Build info
newEntry.setEntryInfo(mDatabase, newEntryInfo) newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Delete temp attachment if not used // Delete temp attachment if not used
mTempAttachments.forEach { mTempAttachments.forEach { tempAttachmentState ->
mDatabase?.binaryPool?.let { binaryPool -> val tempAttachment = tempAttachmentState.attachment
if (!newEntry.getAttachments(binaryPool).contains(it)) { mDatabase?.attachmentPool?.let { binaryPool ->
mDatabase?.removeAttachmentIfNotUsed(it) if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
mDatabase?.removeAttachmentIfNotUsed(tempAttachment)
} }
} }
} }
@@ -619,12 +603,30 @@ class EntryEditActivity : LockingActivity(),
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
MenuUtil.contributionMenuInflater(menuInflater, menu) menuInflater.inflate(R.menu.entry_edit, menu)
return true return true
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_add_field)?.apply {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu?.findItem(R.id.menu_add_attachment)?.isVisible = false
}
menu?.findItem(R.id.menu_add_otp)?.apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
entryEditActivityEducation?.let { entryEditActivityEducation?.let {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) } Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
} }
@@ -676,8 +678,16 @@ class EntryEditActivity : LockingActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_contribute -> { R.id.menu_add_field -> {
MenuUtil.onContributionItemSelected(this) addNewCustomField()
return true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
return true
}
R.id.menu_add_otp -> {
setupOTP()
return true return true
} }
android.R.id.home -> { android.R.id.home -> {
@@ -708,12 +718,6 @@ class EntryEditActivity : LockingActivity(),
} }
} }
override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
entryEditFragment?.icon = icon
}
}
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) { override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue // To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice // https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
@@ -787,6 +791,7 @@ class EntryEditActivity : LockingActivity(),
.setMessage(R.string.discard_changes) .setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ -> .setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true backPressedAlreadyApproved = true
approved.invoke() approved.invoke()
}.create().show() }.create().show()

View File

@@ -52,24 +52,24 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import kotlinx.android.synthetic.main.activity_file_selection.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : SpecialModeActivity(), class FileDatabaseSelectActivity : SpecialModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views // Views
private var coordinatorLayout: CoordinatorLayout? = null private lateinit var coordinatorLayout: CoordinatorLayout
private var createDatabaseButtonView: View? = null private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null private var openDatabaseButtonView: View? = null
@@ -199,8 +199,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (actionTask) { when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> { ACTION_DATABASE_CREATE_TASK -> {
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri -> result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY) val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri) databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
} }
} }
ACTION_DATABASE_LOAD_TASK -> { ACTION_DATABASE_LOAD_TASK -> {
@@ -216,7 +216,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
resultError = "$resultError $resultMessage" resultError = "$resultError $resultMessage"
} }
Log.e(TAG, resultError) Log.e(TAG, resultError)
Snackbar.make(activity_file_selection_coordinator_layout, Snackbar.make(coordinatorLayout,
resultError, resultError,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} }
@@ -237,9 +237,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private fun fileNoFoundAction(e: FileNotFoundException) { private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content) val error = getString(R.string.file_not_found_content)
Log.e(TAG, error, e) Log.e(TAG, error, e)
coordinatorLayout?.let { Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
@@ -330,9 +328,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri) outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
} }
override fun onAssignKeyDialogPositiveClick( override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
try { try {
mDatabaseFileUri?.let { databaseUri -> mDatabaseFileUri?.let { databaseUri ->
@@ -340,24 +336,17 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Create the new database // Create the new database
mProgressDatabaseTaskProvider?.startDatabaseCreate( mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri, databaseUri,
masterPasswordChecked, mainCredential
masterPassword,
keyFileChecked,
keyFile
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error, e) Log.e(TAG, error, e)
} }
} }
override fun onAssignKeyDialogNegativeClick( override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
@@ -380,9 +369,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
.show(supportFragmentManager, "passwordDialog") .show(supportFragmentManager, "passwordDialog")
} else { } else {
val error = getString(R.string.error_create_database) val error = getString(R.string.error_create_database)
coordinatorLayout?.let { Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error) Log.e(TAG, error)
} }
} }

View File

@@ -19,7 +19,9 @@
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog
import android.app.SearchManager import android.app.SearchManager
import android.app.TimePickerDialog
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -33,9 +35,7 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.*
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
@@ -45,6 +45,7 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.fragments.ListNodesFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
@@ -53,37 +54,35 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.* import com.kunzisoft.keepass.view.*
import org.joda.time.DateTime
class GroupActivity : LockingActivity(), class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener, GroupEditDialogFragment.EditGroupListener,
IconPickerDialogFragment.IconPickerListener, DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
ListNodesFragment.NodeClickListener, ListNodesFragment.NodeClickListener,
ListNodesFragment.NodesActionMenuListener, ListNodesFragment.NodesActionMenuListener,
DeleteNodesDialogFragment.DeleteNodeListener, DeleteNodesDialogFragment.DeleteNodeListener,
@@ -105,7 +104,6 @@ class GroupActivity : LockingActivity(),
private var mDatabase: Database? = null private var mDatabase: Database? = null
private var mListNodesFragment: ListNodesFragment? = null private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true private var mRequestStartupSearch = true
private var actionNodeMode: ActionMode? = null private var actionNodeMode: ActionMode? = null
@@ -172,7 +170,7 @@ class GroupActivity : LockingActivity(),
} }
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState) mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action val currentGroupIsASearch = mCurrentGroup?.isVirtual == true
Log.i(TAG, "Started creating tree") Log.i(TAG, "Started creating tree")
if (mCurrentGroup == null) { if (mCurrentGroup == null) {
@@ -181,13 +179,13 @@ class GroupActivity : LockingActivity(),
} }
var fragmentTag = LIST_NODES_FRAGMENT_TAG var fragmentTag = LIST_NODES_FRAGMENT_TAG
if (mCurrentGroupIsASearch) if (currentGroupIsASearch)
fragmentTag = SEARCH_FRAGMENT_TAG fragmentTag = SEARCH_FRAGMENT_TAG
// Initialize the fragment with the list // Initialize the fragment with the list
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment? mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
if (mListNodesFragment == null) if (mListNodesFragment == null)
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch) mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, currentGroupIsASearch)
// Attach fragment to content view // Attach fragment to content view
supportFragmentManager.beginTransaction().replace( supportFragmentManager.beginTransaction().replace(
@@ -206,9 +204,11 @@ class GroupActivity : LockingActivity(),
// Add listeners to the add buttons // Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener { addNodeButtonView?.setAddGroupClickListener {
GroupEditDialogFragment.build() GroupEditDialogFragment.create(GroupInfo().apply {
.show(supportFragmentManager, if (mCurrentGroup?.allowAddNoteInGroup == true) {
GroupEditDialogFragment.TAG_CREATE_GROUP) notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
} }
addNodeButtonView?.setAddEntryClickListener { addNodeButtonView?.setAddEntryClickListener {
mCurrentGroup?.let { currentGroup -> mCurrentGroup?.let { currentGroup ->
@@ -345,13 +345,16 @@ class GroupActivity : LockingActivity(),
} }
ACTION_DATABASE_RELOAD_TASK -> { ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity // Reload the current activity
startActivity(intent) if (result.isSuccess) {
finish() reload()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) } else {
this.showActionErrorIfNeeded(result)
finish()
}
} }
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionErrorIfNeeded(result)
finishNodeAction() finishNodeAction()
@@ -362,6 +365,14 @@ class GroupActivity : LockingActivity(),
Log.i(TAG, "Finished creating tree") Log.i(TAG, "Finished creating tree")
} }
private fun reload() {
// Reload the current activity
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
mDatabase?.wasReloaded = false
}
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
@@ -370,13 +381,10 @@ class GroupActivity : LockingActivity(),
manageSearchInfoIntent(intentNotNull) manageSearchInfoIntent(intentNotNull)
Log.d(TAG, "setNewIntent: $intentNotNull") Log.d(TAG, "setNewIntent: $intentNotNull")
setIntent(intentNotNull) setIntent(intentNotNull)
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) { if (Intent.ACTION_SEARCH == intentNotNull.action) {
// only one instance of search in backstack // only one instance of search in backstack
deletePreviousSearchGroup() deletePreviousSearchGroup()
openGroup(retrieveCurrentGroup(intentNotNull, null), true) openGroup(retrieveCurrentGroup(intentNotNull, null), true)
true
} else {
false
} }
} }
} }
@@ -460,12 +468,11 @@ class GroupActivity : LockingActivity(),
private fun refreshSearchGroup() { private fun refreshSearchGroup() {
deletePreviousSearchGroup() deletePreviousSearchGroup()
if (mCurrentGroupIsASearch) if (mCurrentGroup?.isVirtual == true)
openGroup(retrieveCurrentGroup(intent, null), true) openGroup(retrieveCurrentGroup(intent, null), true)
} }
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? { private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
// Force read only if the database is like that // Force read only if the database is like that
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
@@ -513,24 +520,21 @@ class GroupActivity : LockingActivity(),
} }
} }
} }
if (mCurrentGroupIsASearch) {
searchTitleView?.visibility = View.VISIBLE
} else {
searchTitleView?.visibility = View.GONE
}
// Assign icon if (mCurrentGroup?.isVirtual == true) {
if (mCurrentGroupIsASearch) { searchTitleView?.visibility = View.VISIBLE
if (toolbar != null) { if (toolbar != null) {
toolbar?.navigationIcon = null toolbar?.navigationIcon = null
} }
iconView?.visibility = View.GONE iconView?.visibility = View.GONE
} else { } else {
searchTitleView?.visibility = View.GONE
// Assign the group icon depending of IconPack or custom icon // Assign the group icon depending of IconPack or custom icon
iconView?.visibility = View.VISIBLE iconView?.visibility = View.VISIBLE
mCurrentGroup?.let { mCurrentGroup?.let { currentGroup ->
if (mDatabase?.drawFactory != null) iconView?.let { imageView ->
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor) mDatabase?.iconDrawableFactory?.assignDatabaseIcon(imageView, currentGroup.icon, mIconColor)
}
if (toolbar != null) { if (toolbar != null) {
if (mCurrentGroup?.containsParent() == true) if (mCurrentGroup?.containsParent() == true)
@@ -545,20 +549,25 @@ class GroupActivity : LockingActivity(),
// Assign number of children // Assign number of children
refreshNumberOfChildren() refreshNumberOfChildren()
// Show button if allowed // Hide button
addNodeButtonView?.apply { initAddButton()
}
private fun initAddButton() {
addNodeButtonView?.apply {
closeButtonIfOpen()
// To enable add button // To enable add button
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch val addGroupEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch var addEntryEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
mCurrentGroup?.let { mCurrentGroup?.let {
if (!it.allowAddEntryIfIsRoot()) if (!it.allowAddEntryIfIsRoot)
addEntryEnabled = it != mRootGroup && addEntryEnabled addEntryEnabled = it != mRootGroup && addEntryEnabled
} }
enableAddGroup(addGroupEnabled) enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled) enableAddEntry(addEntryEnabled)
if (mCurrentGroup?.isVirtual == true)
if (actionNodeMode == null) hideButton()
else if (actionNodeMode == null)
showButton() showButton()
} }
} }
@@ -700,6 +709,39 @@ class GroupActivity : LockingActivity(),
) )
} }
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) {
val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment
groupEditFragment?.getExpiryTime()?.date?.let { expiresDate ->
groupEditFragment.setExpiryTime(DateInstant(DateTime(expiresDate)
.withYear(year)
.withMonthOfYear(month + 1)
.withDayOfMonth(day)
.toDate()))
// Launch the time picker
val dateTime = DateTime(expiresDate)
val defaultHour = dateTime.hourOfDay
val defaultMinute = dateTime.minuteOfHour
TimePickerFragment.getInstance(defaultHour, defaultMinute)
.show(supportFragmentManager, "TimePickerFragment")
}
}
}
override fun onTimeSet(view: TimePicker?, hours: Int, minutes: Int) {
val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment
groupEditFragment?.getExpiryTime()?.date?.let { expiresDate ->
// Save the date
groupEditFragment.setExpiryTime(
DateInstant(DateTime(expiresDate)
.withHourOfDay(hours)
.withMinuteOfHour(minutes)
.toDate()))
}
}
private fun finishNodeAction() { private fun finishNodeAction() {
actionNodeMode?.finish() actionNodeMode?.finish()
} }
@@ -745,7 +787,7 @@ class GroupActivity : LockingActivity(),
when (node.type) { when (node.type) {
Type.GROUP -> { Type.GROUP -> {
mOldGroupToUpdate = node as Group mOldGroupToUpdate = node as Group
GroupEditDialogFragment.build(mOldGroupToUpdate!!) GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
.show(supportFragmentManager, .show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP) GroupEditDialogFragment.TAG_CREATE_GROUP)
} }
@@ -855,6 +897,9 @@ class GroupActivity : LockingActivity(),
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (mDatabase?.wasReloaded == true) {
reload()
}
// Show the lock button // Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE View.VISIBLE
@@ -1031,19 +1076,17 @@ class GroupActivity : LockingActivity(),
} }
} }
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
name: String?, groupInfo: GroupInfo) {
icon: IconImage?) {
if (name != null && name.isNotEmpty() && icon != null) { if (groupInfo.title.isNotEmpty()) {
when (action) { when (action) {
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> { GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
// If group creation // If group creation
mCurrentGroup?.let { currentGroup -> mCurrentGroup?.let { currentGroup ->
// Build the group // Build the group
mDatabase?.createGroup()?.let { newGroup -> mDatabase?.createGroup()?.let { newGroup ->
newGroup.title = name newGroup.setGroupInfo(groupInfo)
newGroup.icon = icon
// Not really needed here because added in runnable but safe // Not really needed here because added in runnable but safe
newGroup.parent = currentGroup newGroup.parent = currentGroup
@@ -1063,9 +1106,7 @@ class GroupActivity : LockingActivity(),
// WARNING remove parent and children to keep memory // WARNING remove parent and children to keep memory
removeParent() removeParent()
removeChildren() removeChildren()
this.setGroupInfo(groupInfo)
title = name
this.icon = icon // TODO custom icon #96
} }
} }
// If group updated save it in the database // If group updated save it in the database
@@ -1081,19 +1122,11 @@ class GroupActivity : LockingActivity(),
} }
} }
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
name: String?, groupInfo: GroupInfo) {
icon: IconImage?) {
// Do nothing here // Do nothing here
} }
override// For icon in create tree dialog
fun iconPicked(bundle: Bundle) {
(supportFragmentManager
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
.iconPicked(bundle)
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) { override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters) mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
} }
@@ -1132,6 +1165,13 @@ class GroupActivity : LockingActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
// To create tree dialog for icon
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
(supportFragmentManager
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
.setIcon(icon)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
} }
@@ -1156,7 +1196,6 @@ class GroupActivity : LockingActivity(),
mCurrentGroup = mListNodesFragment?.mainGroup mCurrentGroup = mListNodesFragment?.mainGroup
// Remove search in intent // Remove search in intent
deletePreviousSearchGroup() deletePreviousSearchGroup()
mCurrentGroupIsASearch = false
if (Intent.ACTION_SEARCH == intent.action) { if (Intent.ACTION_SEARCH == intent.action) {
intent.action = Intent.ACTION_DEFAULT intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY) intent.removeExtra(SearchManager.QUERY)

View File

@@ -0,0 +1,324 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.*
class IconPickerActivity : LockingActivity() {
private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var uploadButton: View
private var lockView: View? = null
private var mIconImage: IconImage = IconImage()
private val mainScope = CoroutineScope(Dispatchers.Main)
private val iconPickerViewModel: IconPickerViewModel by viewModels()
private var mCustomIconsSelectionMode = false
private var mIconsSelected: List<IconImageCustom> = ArrayList()
private var mDatabase: Database? = null
private var mSelectFileHelper: SelectFileHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_icon_picker)
mDatabase = Database.getInstance()
toolbar = findViewById(R.id.toolbar)
toolbar.title = " "
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
updateIconsSelectedViews()
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
uploadButton = findViewById(R.id.icon_picker_upload)
if (mDatabase?.allowCustomIcons == true) {
uploadButton.setOnClickListener {
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
uploadButton.setOnLongClickListener {
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
true
}
} else {
uploadButton.visibility = View.GONE
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
mIconImage = it
}
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.icon_picker_fragment, IconPickerFragment.getInstance(
// Default selection tab
if (mIconImage.custom.isUnknown)
IconPickerFragment.IconTab.STANDARD
else
IconPickerFragment.IconTab.CUSTOM
), ICON_PICKER_FRAGMENT_TAG)
}
} else {
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
}
// Focus view to reinitialize timeout
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
mSelectFileHelper = SelectFileHelper(this)
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected
mIconImage.custom = IconImageCustom()
setResult()
finish()
}
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
// Keep the standard icon if a custom one is selected
mIconImage.custom = iconCustom
setResult()
finish()
}
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
mIconsSelected = iconsSelected
updateIconsSelectedViews()
}
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomAdded.errorConsumed = true
}
uploadButton.isEnabled = true
}
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomRemoved.errorConsumed = true
}
uploadButton.isEnabled = true
}
}
private fun updateIconsSelectedViews() {
if (mIconsSelected.isEmpty()) {
mCustomIconsSelectionMode = false
toolbar.title = " "
} else {
mCustomIconsSelectionMode = true
toolbar.title = mIconsSelected.size.toString()
}
invalidateOptionsMenu()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_ICON, mIconImage)
}
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
if (mCustomIconsSelectionMode) {
menuInflater.inflate(R.menu.icon, menu)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
if (mCustomIconsSelectionMode) {
iconPickerViewModel.deselectAllCustomIcons()
} else {
onBackPressed()
}
}
R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove)
}
}
}
return super.onOptionsItemSelected(item)
}
private fun addCustomIcon(iconToUploadUri: Uri?) {
uploadButton.isEnabled = false
mainScope.launch {
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
if (documentFile.length() > MAX_ICON_SIZE) {
iconCustomState.errorStringId = R.string.error_file_to_big
} else {
mDatabase?.buildNewCustomIcon(UriUtil.getBinaryDir(this@IconPickerActivity)) { customIcon, binary ->
if (customIcon != null) {
iconCustomState.iconCustom = customIcon
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(contentResolver,
iconToUploadUri, binary)
when {
binary == null -> {
}
binary.getSize() <= 0 -> {
}
mDatabase?.isCustomIconBinaryDuplicate(binary) == true -> {
iconCustomState.errorStringId = R.string.error_duplicate_file
}
else -> {
iconCustomState.error = false
}
}
if (iconCustomState.error) {
mDatabase?.removeCustomIcon(customIcon)
}
}
}
}
}
iconCustomState
}
withContext(Dispatchers.Main) {
asyncResult.await()?.let { customIcon ->
iconPickerViewModel.addCustomIcon(customIcon)
}
}
}
}
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false
iconPickerViewModel.deselectAllCustomIcons()
mDatabase?.removeCustomIcon(iconImageCustom)
iconPickerViewModel.removeCustomIcon(
IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file)
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}
private fun setResult() {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_ICON, mIconImage)
})
}
override fun onBackPressed() {
setResult()
super.onBackPressed()
}
companion object {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val ICON_SELECTED_REQUEST = 15861
private const val EXTRA_ICON = "EXTRA_ICON"
private const val MAX_ICON_SIZE = 5242880
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
if (requestCode == ICON_SELECTED_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
}
}
}
fun launch(context: Activity,
previousIcon: IconImage?) {
// Create an instance to return the picker icon
context.startActivityForResult(
Intent(context,
IconPickerActivity::class.java).apply {
if (previousIcon != null)
putExtra(EXTRA_ICON, previousIcon)
},
ICON_SELECTED_REQUEST)
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.format.Formatter
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.widget.Toolbar
import com.igreenwood.loupe.Loupe
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import kotlin.math.max
class ImageViewerActivity : LockingActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_image_viewer)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
val imageView: ImageView = findViewById(R.id.image_viewer_image)
val progressView: View = findViewById(R.id.image_viewer_progress)
// Approximately, to not OOM and allow a zoom
val mImagePreviewMaxWidth = max(
resources.displayMetrics.widthPixels * 2,
resources.displayMetrics.heightPixels * 2
)
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryData.getSize())
BinaryDatabaseManager.loadBitmap(
attachment.binaryData,
Database.getInstance().loadedCipherKey,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
} ?: finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e)
finish()
}
Loupe.create(imageView, imageContainerView) {
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
override fun onStart(view: ImageView) {
// called when the view starts moving
}
override fun onViewTranslate(view: ImageView, amount: Float) {
// called whenever the view position changed
}
override fun onRestore(view: ImageView) {
// called when the view drag gesture ended
}
override fun onDismiss(view: ImageView) {
// called when the view drag gesture ended
finish()
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
companion object {
private val TAG = ImageViewerActivity::class.simpleName
private const val IMAGE_ATTACHMENT_TAG = "IMAGE_ATTACHMENT_TAG"
fun getInstance(context: Context, imageAttachment: Attachment) {
context.startActivity(Intent(context, ImageViewerActivity::class.java).apply {
putExtra(IMAGE_ATTACHMENT_TAG, imageAttachment)
})
}
}
}

View File

@@ -36,6 +36,7 @@ import android.widget.*
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -56,14 +57,14 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
@@ -71,7 +72,6 @@ import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener { open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
@@ -84,8 +84,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var infoContainerView: ViewGroup? = null private var infoContainerView: ViewGroup? = null
private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels() private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
@@ -131,6 +132,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
@@ -236,15 +238,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
showLoadDatabaseDuplicateUuidMessage { showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null var databaseUri: Uri? = null
var masterPassword: String? = null var mainCredential: MainCredential = MainCredential()
var keyFileUri: Uri? = null
var readOnly = true var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData -> result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY) databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY) mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY) readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY) cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
} }
@@ -252,8 +252,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
databaseUri?.let { databaseFileUri -> databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseFileUri, databaseFileUri,
masterPassword, mainCredential,
keyFileUri,
readOnly, readOnly,
cipherEntity, cipherEntity,
true) true)
@@ -274,7 +273,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
resultError = "$resultError $resultMessage" resultError = "$resultError $resultMessage"
} }
Log.e(TAG, resultError) Log.e(TAG, resultError)
Snackbar.make(activity_password_coordinator_layout, Snackbar.make(coordinatorLayout,
resultError, resultError,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} }
@@ -526,7 +525,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|| mSpecialMode == SpecialMode.REGISTRATION) || mSpecialMode == SpecialMode.REGISTRATION)
) { ) {
Log.e(TAG, getString(R.string.autofill_read_only_save)) Log.e(TAG, getString(R.string.autofill_read_only_save))
Snackbar.make(activity_password_coordinator_layout, Snackbar.make(coordinatorLayout,
R.string.autofill_read_only_save, R.string.autofill_read_only_save,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} else { } else {
@@ -534,8 +533,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
// Show the progress dialog and load the database // Show the progress dialog and load the database
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseUri, databaseUri,
password, MainCredential(password, keyFileUri),
keyFileUri,
readOnly, readOnly,
cipherDatabaseEntity, cipherDatabaseEntity,
false) false)
@@ -544,15 +542,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
} }
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
password: String?, mainCredential: MainCredential,
keyFile: Uri?,
readOnly: Boolean, readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?, cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
mProgressDatabaseTaskProvider?.startDatabaseLoad( mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri, databaseUri,
password, mainCredential,
keyFile,
readOnly, readOnly,
cipherDatabaseEntity, cipherDatabaseEntity,
fixDuplicateUUID fixDuplicateUUID

View File

@@ -37,6 +37,7 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
@@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
} }
interface AssignPasswordDialogListener { interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
keyFileChecked: Boolean, keyFile: Uri?) fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
} }
override fun onAttach(activity: Context) { override fun onAttach(activity: Context) {
@@ -121,8 +120,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information) rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
credentialsInfo?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url) UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
} }
@@ -161,17 +159,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
} }
} }
if (!error) { if (!error) {
mListener?.onAssignKeyDialogPositiveClick( mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
dismiss() dismiss()
} }
} }
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener { negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick( mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
dismiss() dismiss()
} }
} }
@@ -183,6 +177,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
} }
private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
return MainCredential(masterPassword, keyFile)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@@ -242,9 +242,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
builder.setMessage(R.string.warning_empty_password) builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) { if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick( mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss() this@AssignMasterKeyDialogFragment.dismiss()
} }
} }
@@ -259,9 +257,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(it) val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_no_encryption_key) builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick( mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss() this@AssignMasterKeyDialogFragment.dismiss()
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }

View File

@@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
open class DeleteNodesDialogFragment : DialogFragment() { open class DeleteNodesDialogFragment : DialogFragment() {

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
// Use the Builder class for convenient dialog construction // Use the Builder class for convenient dialog construction
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply { val builder = AlertDialog.Builder(activity).apply {
val message = getString(R.string.contains_duplicate_uuid) + val message = getString(R.string.contains_duplicate_uuid) +
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure) "\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
setMessage(message) setMessage(message)

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() { class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {

View File

@@ -23,34 +23,40 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.IconPickerActivity
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.ExpirationView
import org.joda.time.DateTime
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener { class GroupEditDialogFragment : DialogFragment() {
private var mDatabase: Database? = null private var mDatabase: Database? = null
private var editGroupListener: EditGroupListener? = null private var mEditGroupListener: EditGroupListener? = null
private var editGroupDialogAction: EditGroupDialogAction? = null private var mEditGroupDialogAction = EditGroupDialogAction.NONE
private var nameGroup: String? = null private var mGroupInfo = GroupInfo()
private var iconGroup: IconImage? = null
private var nameTextLayoutView: TextInputLayout? = null private lateinit var iconButtonView: ImageView
private var nameTextView: TextView? = null
private var iconButtonView: ImageView? = null
private var iconColor: Int = 0 private var iconColor: Int = 0
private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView
private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView
private lateinit var expirationView: ExpirationView
enum class EditGroupDialogAction { enum class EditGroupDialogAction {
CREATION, UPDATE, NONE; CREATION, UPDATE, NONE;
@@ -67,7 +73,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
// Verify that the host activity implements the callback interface // Verify that the host activity implements the callback interface
try { try {
// Instantiate the NoticeDialogListener so we can send events to the host // Instantiate the NoticeDialogListener so we can send events to the host
editGroupListener = context as EditGroupListener mEditGroupListener = context as EditGroupListener
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception // The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString() throw ClassCastException(context.toString()
@@ -76,16 +82,19 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
} }
override fun onDetach() { override fun onDetach() {
editGroupListener = null mEditGroupListener = null
super.onDetach() super.onDetach()
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null) val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container) iconButtonView = root.findViewById(R.id.group_edit_icon_button)
nameTextView = root?.findViewById(R.id.group_edit_name) nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
iconButtonView = root?.findViewById(R.id.group_edit_icon_button) nameTextView = root.findViewById(R.id.group_edit_name)
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
notesTextView = root.findViewById(R.id.group_edit_note)
expirationView = root.findViewById(R.id.group_edit_expiration)
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
@@ -94,47 +103,47 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
// Init elements // Init elements
mDatabase = Database.getInstance() mDatabase = Database.getInstance()
editGroupDialogAction = EditGroupDialogAction.NONE
nameGroup = ""
iconGroup = mDatabase?.iconFactory?.folderIcon
if (savedInstanceState != null if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ACTION_ID) && savedInstanceState.containsKey(KEY_ACTION_ID)
&& savedInstanceState.containsKey(KEY_NAME) && savedInstanceState.containsKey(KEY_GROUP_INFO)) {
&& savedInstanceState.containsKey(KEY_ICON)) { mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID)) mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
nameGroup = savedInstanceState.getString(KEY_NAME)
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
} else { } else {
arguments?.apply { arguments?.apply {
if (containsKey(KEY_ACTION_ID)) if (containsKey(KEY_ACTION_ID))
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID)) mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
if (containsKey(KEY_GROUP_INFO)) {
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) { mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
nameGroup = getString(KEY_NAME)
iconGroup = getParcelable(KEY_ICON)
} }
} }
} }
// populate the name // populate info in views
nameTextView?.text = nameGroup populateInfoToViews()
// populate the icon expirationView.setOnDateClickListener = {
assignIconView() expirationView.expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(parentFragmentManager, "DatePickerFragment")
}
}
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.setView(root) builder.setView(root)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
editGroupListener?.cancelEditGroup( retrieveGroupInfoFromViews()
editGroupDialogAction, mEditGroupListener?.cancelEditGroup(
nameTextView?.text?.toString(), mEditGroupDialogAction,
iconGroup) mGroupInfo)
} }
iconButtonView?.setOnClickListener { _ -> iconButtonView.setOnClickListener { _ ->
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment") IconPickerActivity.launch(activity, mGroupInfo.icon)
} }
return builder.create() return builder.create()
@@ -150,69 +159,99 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
if (d != null) { if (d != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener { positiveButton.setOnClickListener {
retrieveGroupInfoFromViews()
if (isValid()) { if (isValid()) {
editGroupListener?.approveEditGroup( mEditGroupListener?.approveEditGroup(
editGroupDialogAction, mEditGroupDialogAction,
nameTextView?.text?.toString(), mGroupInfo)
iconGroup)
d.dismiss() d.dismiss()
} }
} }
} }
} }
private fun assignIconView() { fun getExpiryTime(): DateInstant {
if (mDatabase?.drawFactory != null && iconGroup != null) { retrieveGroupInfoFromViews()
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor) return mGroupInfo.expiryTime
}
} }
override fun iconPicked(bundle: Bundle) { fun setExpiryTime(expiryTime: DateInstant) {
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle) mGroupInfo.expiryTime = expiryTime
populateInfoToViews()
}
private fun populateInfoToViews() {
assignIconView()
nameTextView.text = mGroupInfo.title
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
mGroupInfo.notes?.let {
notesTextView.text = it
}
expirationView.expires = mGroupInfo.expires
expirationView.expiryTime = mGroupInfo.expiryTime
}
private fun retrieveGroupInfoFromViews() {
mGroupInfo.title = nameTextView.text.toString()
// Only if there
val newNotes = notesTextView.text.toString()
if (newNotes.isNotEmpty()) {
mGroupInfo.notes = newNotes
}
mGroupInfo.expires = expirationView.expires
mGroupInfo.expiryTime = expirationView.expiryTime
}
private fun assignIconView() {
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
}
fun setIcon(icon: IconImage) {
mGroupInfo.icon = icon
assignIconView() assignIconView()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal) retrieveGroupInfoFromViews()
outState.putString(KEY_NAME, nameGroup) outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
outState.putParcelable(KEY_ICON, iconGroup) outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun isValid(): Boolean { private fun isValid(): Boolean {
if (nameTextView?.text?.toString()?.isNotEmpty() != true) { if (nameTextView.text.toString().isEmpty()) {
nameTextLayoutView?.error = getString(R.string.error_no_name) nameTextLayoutView.error = getString(R.string.error_no_name)
return false return false
} }
return true return true
} }
interface EditGroupListener { interface EditGroupListener {
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?) fun approveEditGroup(action: EditGroupDialogAction,
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?) groupInfo: GroupInfo)
fun cancelEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo)
} }
companion object { companion object {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
const val KEY_NAME = "KEY_NAME"
const val KEY_ICON = "KEY_ICON"
const val KEY_ACTION_ID = "KEY_ACTION_ID" const val KEY_ACTION_ID = "KEY_ACTION_ID"
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun build(): GroupEditDialogFragment { fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal) bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupEditDialogFragment() val fragment = GroupEditDialogFragment()
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment
} }
fun build(group: Group): GroupEditDialogFragment { fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()
bundle.putString(KEY_NAME, group.title)
bundle.putParcelable(KEY_ICON, group.icon)
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal) bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupEditDialogFragment() val fragment = GroupEditDialogFragment()
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment

View File

@@ -1,147 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.GridView
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.icons.IconPack
import com.kunzisoft.keepass.icons.IconPackChooser
class IconPickerDialogFragment : DialogFragment() {
private var iconPickerListener: IconPickerListener? = null
private var iconPack: IconPack? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
iconPickerListener = context as IconPickerListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + IconPickerListener::class.java.name)
}
}
override fun onDetach() {
iconPickerListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
builder.setView(root)
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
currIconGridView.adapter = ImageAdapter(activity)
currIconGridView.setOnItemClickListener { _, _, position, _ ->
val bundle = Bundle()
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
iconPickerListener?.iconPicked(bundle)
dismiss()
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
override fun getCount(): Int {
return iconPack?.numberOfIcons() ?: 0
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val currentView: View = convertView
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.item_icon, parent, false)
iconPack?.let { iconPack ->
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
iconImageView.setImageResource(iconPack.iconToResId(position))
// Assign color if icons are tintable
if (iconPack.tintable()) {
// Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta.recycle()
}
}
return currentView
}
}
interface IconPickerListener {
fun iconPicked(bundle: Bundle)
}
companion object {
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
return bundle.getParcelable(KEY_ICON_STANDARD)
}
fun launch(activity: FragmentActivity) {
// Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
}
}
}

View File

@@ -26,6 +26,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() { class PasswordEncodingDialogFragment : DialogFragment() {
@@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY) val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
activity?.let { activity -> activity?.let { activity ->
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
@@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onPasswordEncodingValidateListener( mListener?.onPasswordEncodingValidateListener(
databaseUri, databaseUri,
masterPasswordChecked, mainCredential
masterPassword,
keyFileChecked,
keyFile
) )
} }
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
@@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() {
interface Listener { interface Listener {
fun onPasswordEncodingValidateListener(databaseUri: Uri?, fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean, mainCredential: MainCredential)
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?)
} }
companion object { companion object {
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY" private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
fun getInstance(databaseUri: Uri, fun getInstance(databaseUri: Uri,
masterPasswordChecked: Boolean, mainCredential: MainCredential): SortDialogFragment {
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?): SortDialogFragment {
val fragment = SortDialogFragment() val fragment = SortDialogFragment()
fragment.arguments = Bundle().apply { fragment.arguments = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(DATABASE_URI_KEY, databaseUri)
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) putParcelable(MAIN_CREDENTIAL, mainCredential)
putString(MASTER_PASSWORD_KEY, masterPassword)
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(KEY_FILE_URI_KEY, keyFile)
} }
return fragment return fragment
} }

View File

@@ -46,6 +46,7 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpTokenType import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.utils.UriUtil
import java.util.* import java.util.*
class SetOTPDialogFragment : DialogFragment() { class SetOTPDialogFragment : DialogFragment() {
@@ -223,13 +224,16 @@ class SetOTPDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.apply { builder.apply {
setTitle(R.string.entry_setup_otp)
setView(root) setView(root)
.setPositiveButton(android.R.string.ok) {_, _ -> } .setPositiveButton(android.R.string.ok) {_, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
} }
} }
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
}
return builder.create() return builder.create()
} }
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
@@ -26,29 +26,29 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.CompoundButton
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ExpirationView
import com.kunzisoft.keepass.view.applyFontVisibility import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand import com.kunzisoft.keepass.view.expand
@@ -63,8 +63,7 @@ class EntryEditFragment: StylishFragment() {
private lateinit var entryPasswordLayoutView: TextInputLayout private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View private lateinit var entryPasswordGeneratorView: View
private lateinit var entryExpiresCheckBox: CompoundButton private lateinit var entryExpirationView: ExpirationView
private lateinit var entryExpiresTextView: TextView
private lateinit var entryNotesView: EditText private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup private lateinit var extraFieldsListView: ViewGroup
@@ -75,17 +74,17 @@ class EntryEditFragment: StylishFragment() {
private var fontInVisibility: Boolean = false private var fontInVisibility: Boolean = false
private var iconColor: Int = 0 private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
var drawFactory: IconDrawableFactory? = null var drawFactory: IconDrawableFactory? = null
var setOnDateClickListener: View.OnClickListener? = null var setOnDateClickListener: (() -> Unit)? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: View.OnClickListener? = null var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
var setOnEditCustomField: ((Field) -> Unit)? = null var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry // Elements to modify the current entry
private var mEntryInfo = EntryInfo() private var mEntryInfo = EntryInfo()
private var mBinaryCipherKey: Database.LoadedKey? = null
private var mLastFocusedEditField: FocusedEditField? = null private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null private var mExtraViewToRequestFocus: EditText? = null
@@ -101,7 +100,7 @@ class EntryEditFragment: StylishFragment() {
entryTitleView = rootView.findViewById(R.id.entry_edit_title) entryTitleView = rootView.findViewById(R.id.entry_edit_title)
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button) entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener { entryIconView.setOnClickListener {
setOnIconViewClickListener?.onClick(it) setOnIconViewClickListener?.invoke(mEntryInfo.icon)
} }
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name) entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
@@ -112,12 +111,8 @@ class EntryEditFragment: StylishFragment() {
entryPasswordGeneratorView.setOnClickListener { entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it) setOnPasswordGeneratorClickListener?.onClick(it)
} }
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox) entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text) entryExpirationView.setOnDateClickListener = setOnDateClickListener
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.onClick(it)
}
entryNotesView = rootView.findViewById(R.id.entry_edit_notes) entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
@@ -127,6 +122,7 @@ class EntryEditFragment: StylishFragment() {
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container) attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list) attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey?
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize -> attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) { if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true) attachmentsContainerView.collapse(true)
@@ -140,10 +136,6 @@ class EntryEditFragment: StylishFragment() {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
@@ -178,7 +170,7 @@ class EntryEditFragment: StylishFragment() {
setOnEditCustomField = null setOnEditCustomField = null
} }
fun getEntryInfo(): EntryInfo? { fun getEntryInfo(): EntryInfo {
populateEntryWithViews() populateEntryWithViews()
return mEntryInfo return mEntryInfo
} }
@@ -247,9 +239,7 @@ class EntryEditFragment: StylishFragment() {
} }
set(value) { set(value) {
mEntryInfo.icon = value mEntryInfo.icon = value
drawFactory?.let { drawFactory -> drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
}
} }
var username: String var username: String
@@ -283,41 +273,20 @@ class EntryEditFragment: StylishFragment() {
} }
} }
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean var expires: Boolean
get() { get() {
return entryExpiresCheckBox.isChecked return entryExpirationView.expires
} }
set(value) { set(value) {
if (!value) { entryExpirationView.expires = value
expiresInstant = DateInstant.IN_ONE_MONTH
}
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
} }
var expiryTime: DateInstant var expiryTime: DateInstant
get() { get() {
return if (expires) return entryExpirationView.expiryTime
expiresInstant
else
DateInstant.NEVER_EXPIRE
} }
set(value) { set(value) {
if (expires) entryExpirationView.expiryTime = value
expiresInstant = value
assignExpiresDateText()
} }
var notes: String var notes: String
@@ -344,7 +313,8 @@ class EntryEditFragment: StylishFragment() {
itemView?.id = View.NO_ID itemView?.id = View.NO_ID
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container) val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
extraFieldValueContainer?.hint = extraField.name extraFieldValueContainer?.hint = extraField.name
extraFieldValueContainer?.id = View.NO_ID extraFieldValueContainer?.id = View.NO_ID
@@ -502,9 +472,13 @@ class EntryEditFragment: StylishFragment() {
return attachmentsAdapter.contains(attachment) return attachmentsAdapter.contains(attachment)
} }
fun putAttachment(attachment: EntryAttachmentState) { fun putAttachment(attachment: EntryAttachmentState,
onPreviewLoaded: (()-> Unit)? = null) {
attachmentsContainerView.visibility = View.VISIBLE attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment) attachmentsAdapter.putItem(attachment)
attachmentsAdapter.onBinaryPreviewLoaded = {
onPreviewLoaded?.invoke()
}
} }
fun removeAttachment(attachment: EntryAttachmentState) { fun removeAttachment(attachment: EntryAttachmentState) {
@@ -528,6 +502,7 @@ class EntryEditFragment: StylishFragment() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews() populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo) outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField) outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@@ -535,12 +510,15 @@ class EntryEditFragment: StylishFragment() {
companion object { companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO" const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_BINARY_CIPHER_KEY = "KEY_BINARY_CIPHER_KEY"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD" const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment { fun getInstance(entryInfo: EntryInfo?,
loadedKey: Database.LoadedKey?): EntryEditFragment {
return EntryEditFragment().apply { return EntryEditFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo) putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey)
} }
} }
} }

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
class IconCustomFragment : IconFragment<IconImageCustom>() {
override fun retrieveMainLayoutId(): Int {
return R.layout.fragment_icon_grid
}
override fun defineIconList() {
mDatabase?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected ->
if (customIconsSelected.isEmpty()) {
iconActionSelectionMode = false
iconPickerAdapter.deselectAllIcons()
} else {
iconActionSelectionMode = true
iconPickerAdapter.updateIconSelectedState(customIconsSelected)
}
}
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustomAdded ->
if (!iconCustomAdded.error) {
iconCustomAdded?.iconCustom?.let { icon ->
iconPickerAdapter.addIcon(icon)
iconCustomAdded.iconCustom = null
}
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
}
}
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
if (!iconCustomRemoved.error) {
iconCustomRemoved?.iconCustom?.let { icon ->
iconPickerAdapter.removeIcon(icon)
iconCustomRemoved.iconCustom = null
}
}
}
}
override fun onIconClickListener(icon: IconImageCustom) {
if (iconActionSelectionMode) {
// Same long click behavior after each single click
onIconLongClickListener(icon)
} else {
iconPickerViewModel.pickCustomIcon(icon)
}
}
override fun onIconLongClickListener(icon: IconImageCustom) {
// Select or deselect item if already selected
icon.selected = !icon.selected
iconPickerAdapter.updateIcon(icon)
iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon()
iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons())
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
protected var iconActionSelectionMode = false
protected var mDatabase: Database? = null
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
abstract fun retrieveMainLayoutId(): Int
abstract fun defineIconList()
override fun onAttach(context: Context) {
super.onAttach(context)
mDatabase = Database.getInstance()
// Retrieve the textColor to tint the icon
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle()
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory
}
CoroutineScope(Dispatchers.IO).launch {
val populateList = launch {
iconPickerAdapter.clear()
defineIconList()
}
withContext(Dispatchers.Main) {
populateList.join()
iconPickerAdapter.notifyDataSetChanged()
}
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
iconsGridView = root.findViewById(R.id.icons_grid_view)
iconsGridView.adapter = iconPickerAdapter
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerAdapter.iconPickerListener = this
}
fun onIconDeleteClicked() {
iconActionSelectionMode = false
}
}

View File

@@ -0,0 +1,77 @@
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : StylishFragment() {
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
private lateinit var viewPager: ViewPager2
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mDatabase = Database.getInstance()
viewPager = view.findViewById(R.id.icon_picker_pager)
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (mDatabase?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
1 -> getString(R.string.icon_section_custom)
else -> getString(R.string.icon_section_standard)
}
}.attach()
arguments?.apply {
if (containsKey(ICON_TAB_ARG)) {
viewPager.currentItem = getInt(ICON_TAB_ARG)
}
remove(ICON_TAB_ARG)
}
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
viewPager.currentItem = 1
}
}
enum class IconTab {
STANDARD, CUSTOM
}
companion object {
private const val ICON_TAB_ARG = "ICON_TAB_ARG"
fun getInstance(iconTab: IconTab): IconPickerFragment {
val fragment = IconPickerFragment()
fragment.arguments = Bundle().apply {
putInt(ICON_TAB_ARG, iconTab.ordinal)
}
return fragment
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
class IconStandardFragment : IconFragment<IconImageStandard>() {
override fun retrieveMainLayoutId(): Int {
return R.layout.fragment_icon_grid
}
override fun defineIconList() {
mDatabase?.doForEachStandardIcons { standardIcon ->
iconPickerAdapter.addIcon(standardIcon, false)
}
}
override fun onIconClickListener(icon: IconImageStandard) {
iconPickerViewModel.pickStandardIcon(icon)
}
override fun onIconLongClickListener(icon: IconImageStandard) {}
}

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -28,6 +28,7 @@ import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper

View File

@@ -20,11 +20,11 @@
package com.kunzisoft.keepass.activities.stylish package com.kunzisoft.keepass.activities.stylish
import android.content.Context import android.content.Context
import androidx.annotation.StyleRes import android.content.res.Configuration
import androidx.preference.PreferenceManager
import android.util.Log import android.util.Log
import androidx.annotation.StyleRes
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
/** /**
* Class that provides functions to retrieve and assign a theme to a module * Class that provides functions to retrieve and assign a theme to a module
@@ -38,17 +38,58 @@ object Stylish {
* @param context Context to retrieve the theme preference * @param context Context to retrieve the theme preference
*/ */
fun init(context: Context) { fun init(context: Context) {
val stylishPrefKey = context.getString(R.string.setting_style_key)
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName) Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light)) themeString = PreferencesUtil.getStyle(context)
}
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
context.getString(R.string.list_style_brightness_light) -> false
context.getString(R.string.list_style_brightness_night) -> true
else -> {
when (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
Configuration.UI_MODE_NIGHT_YES -> true
else -> false
}
}
}
return if (systemNightMode) {
retrieveEquivalentNightStyle(context, styleString)
} else {
retrieveEquivalentLightStyle(context, styleString)
}
}
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
return when (styleString) {
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
else -> styleString
}
}
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
return when (styleString) {
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
else -> styleString
}
} }
/** /**
* Assign the style to the class attribute * Assign the style to the class attribute
* @param styleString Style id String * @param styleString Style id String
*/ */
fun assignStyle(styleString: String) { fun assignStyle(context: Context, styleString: String) {
themeString = styleString themeString = retrieveEquivalentSystemStyle(context, styleString)
} }
/** /**
@@ -58,14 +99,18 @@ object Stylish {
*/ */
@StyleRes @StyleRes
fun getThemeId(context: Context): Int { fun getThemeId(context: Context): Int {
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
return when (themeString) {
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
else -> R.style.KeepassDXStyle_Light else -> R.style.KeepassDXStyle_Light
} }
} }

View File

@@ -42,6 +42,7 @@ abstract class StylishFragment : Fragment() {
contextThemed = ContextThemeWrapper(context, themeId) contextThemed = ContextThemeWrapper(context, themeId)
} }
@Suppress("DEPRECATION")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color // To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -53,14 +54,21 @@ abstract class StylishFragment : Fragment() {
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle() taStatusBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
taWindowStatusLight?.recycle()
} catch (e: Exception) {}
}
try { try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor)) val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle() taNavigationBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {}
} }
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }

View File

@@ -120,7 +120,9 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
} }
fun clear() { fun clear() {
itemsList.clear() if (itemsList.size > 0) {
notifyDataSetChanged() itemsList.clear()
notifyDataSetChanged()
}
} }
} }

View File

@@ -31,17 +31,29 @@ import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.ImageViewerActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.view.expand
import kotlin.math.max
class EntryAttachmentsItemsAdapter(context: Context) class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) { : AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var binaryCipherKey: Database.LoadedKey? = null
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
// Approximately
private val mImagePreviewMaxWidth = max(
context.resources.displayMetrics.widthPixels,
context.resources.getDimensionPixelSize(R.dimen.item_file_info_height)
)
private var mTitleColor: Int private var mTitleColor: Int
init { init {
@@ -62,24 +74,59 @@ class EntryAttachmentsItemsAdapter(context: Context)
val entryAttachmentState = itemsList[position] val entryAttachmentState = itemsList[position]
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.binaryFileThumbnail.apply {
// Perform image loading only if upload is finished
if (entryAttachmentState.downloadState != AttachmentState.START
&& entryAttachmentState.downloadState != AttachmentState.IN_PROGRESS) {
// Show the bitmap image if loaded
if (entryAttachmentState.previewState == AttachmentState.NULL) {
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
// Load the bitmap image
BinaryDatabaseManager.loadBitmap(
entryAttachmentState.attachment.binaryData,
binaryCipherKey,
mImagePreviewMaxWidth
) { imageLoaded ->
if (imageLoaded == null) {
entryAttachmentState.previewState = AttachmentState.ERROR
visibility = View.GONE
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
} else {
entryAttachmentState.previewState = AttachmentState.COMPLETE
setImageBitmap(imageLoaded)
if (visibility != View.VISIBLE) {
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) {
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
}
}
}
}
}
} else {
visibility = View.GONE
}
this.setOnClickListener {
ImageViewerActivity.getInstance(context, entryAttachmentState.attachment)
}
}
holder.binaryFileBroken.apply { holder.binaryFileBroken.apply {
setColorFilter(Color.RED) setColorFilter(Color.RED)
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { visibility = if (entryAttachmentState.attachment.binaryData.isCorrupted) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
} }
} }
holder.binaryFileTitle.text = entryAttachmentState.attachment.name holder.binaryFileTitle.text = entryAttachmentState.attachment.name
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { if (entryAttachmentState.attachment.binaryData.isCorrupted) {
holder.binaryFileTitle.setTextColor(Color.RED) holder.binaryFileTitle.setTextColor(Color.RED)
} else { } else {
holder.binaryFileTitle.setTextColor(mTitleColor) holder.binaryFileTitle.setTextColor(mTitleColor)
} }
holder.binaryFileSize.text = Formatter.formatFileSize(context, holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachmentState.attachment.binaryAttachment.length()) entryAttachmentState.attachment.binaryData.getSize())
holder.binaryFileCompression.apply { holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) { if (entryAttachmentState.attachment.binaryData.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources) text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE visibility = View.VISIBLE
} else { } else {
@@ -105,6 +152,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
} }
AttachmentState.NULL, AttachmentState.NULL,
AttachmentState.ERROR, AttachmentState.ERROR,
AttachmentState.CANCELED,
AttachmentState.COMPLETE -> { AttachmentState.COMPLETE -> {
holder.binaryFileProgressContainer.visibility = View.GONE holder.binaryFileProgressContainer.visibility = View.GONE
holder.binaryFileProgress.visibility = View.GONE holder.binaryFileProgress.visibility = View.GONE
@@ -114,7 +162,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
} }
} }
} }
holder.itemView.setOnClickListener(null) holder.binaryFileInfo.setOnClickListener(null)
} }
StreamDirection.DOWNLOAD -> { StreamDirection.DOWNLOAD -> {
holder.binaryFileProgressIcon.isActivated = false holder.binaryFileProgressIcon.isActivated = false
@@ -122,12 +170,17 @@ class EntryAttachmentsItemsAdapter(context: Context)
holder.binaryFileDeleteButton.visibility = View.GONE holder.binaryFileDeleteButton.visibility = View.GONE
holder.binaryFileProgress.apply { holder.binaryFileProgress.apply {
visibility = when (entryAttachmentState.downloadState) { visibility = when (entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE AttachmentState.NULL,
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE AttachmentState.COMPLETE,
AttachmentState.CANCELED,
AttachmentState.ERROR -> View.GONE
AttachmentState.START,
AttachmentState.IN_PROGRESS -> View.VISIBLE
} }
progress = entryAttachmentState.downloadProgression progress = entryAttachmentState.downloadProgression
} }
holder.itemView.setOnClickListener { holder.binaryFileInfo.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState) onItemClickListener?.invoke(entryAttachmentState)
} }
} }
@@ -136,6 +189,8 @@ class EntryAttachmentsItemsAdapter(context: Context)
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binaryFileThumbnail: ImageView = itemView.findViewById(R.id.item_attachment_thumbnail)
var binaryFileInfo: View = itemView.findViewById(R.id.item_attachment_info)
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken) var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title) var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size) var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)

View File

@@ -0,0 +1,121 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import com.kunzisoft.keepass.icons.IconDrawableFactory
class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tintIcon: Int)
: RecyclerView.Adapter<IconPickerAdapter<I>.CustomIconViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val iconList = ArrayList<I>()
var iconDrawableFactory: IconDrawableFactory? = null
var iconPickerListener: IconPickerListener<I>? = null
val lastPosition: Int
get() = iconList.lastIndex
fun addIcon(icon: I, notify: Boolean = true) {
if (!iconList.contains(icon)) {
iconList.add(icon)
if (notify) {
notifyItemInserted(iconList.indexOf(icon))
}
}
}
fun updateIcon(icon: I) {
val index = iconList.indexOf(icon)
if (index != -1) {
iconList[index] = icon
notifyItemChanged(index)
}
}
fun updateIconSelectedState(icons: List<I>) {
icons.forEach { icon ->
val index = iconList.indexOf(icon)
if (index != -1
&& iconList[index].selected != icon.selected) {
iconList[index] = icon
notifyItemChanged(index)
}
}
}
fun removeIcon(icon: I) {
if (iconList.contains(icon)) {
val position = iconList.indexOf(icon)
iconList.remove(icon)
notifyItemRemoved(position)
}
}
fun containsAnySelectedIcon(): Boolean {
return iconList.firstOrNull { it.selected } != null
}
fun deselectAllIcons() {
iconList.forEachIndexed { index, icon ->
if (icon.selected) {
icon.selected = false
notifyItemChanged(index)
}
}
}
fun getSelectedIcons(): List<I> {
return iconList.filter { it.selected }
}
fun clear() {
iconList.clear()
}
fun setList(icons: List<I>) {
iconList.clear()
icons.forEach { iconImage ->
iconList.add(iconImage)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomIconViewHolder {
val view = inflater.inflate(R.layout.item_icon, parent, false)
return CustomIconViewHolder(view)
}
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
val icon = iconList[position]
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
holder.iconContainerView.isSelected = icon.selected
holder.itemView.setOnClickListener {
iconPickerListener?.onIconClickListener(icon)
}
holder.itemView.setOnLongClickListener {
iconPickerListener?.onIconLongClickListener(icon)
true
}
}
override fun getItemCount(): Int {
return iconList.size
}
interface IconPickerListener<I: IconImageDraw> {
fun onIconClickListener(icon: I)
fun onIconLongClickListener(icon: I)
}
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
}
}

View File

@@ -0,0 +1,24 @@
package com.kunzisoft.keepass.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.kunzisoft.keepass.activities.fragments.IconCustomFragment
import com.kunzisoft.keepass.activities.fragments.IconStandardFragment
class IconPickerPagerAdapter(fragment: Fragment, val size: Int)
: FragmentStateAdapter(fragment) {
private val iconStandardFragment = IconStandardFragment()
private val iconCustomFragment = IconCustomFragment()
override fun getItemCount(): Int {
return size
}
override fun createFragment(position: Int): Fragment {
return when (position) {
1 -> iconCustomFragment
else -> iconStandardFragment
}
}
}

View File

@@ -28,6 +28,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -39,7 +40,6 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
@@ -57,7 +57,7 @@ class NodeAdapter (private val context: Context)
private val mNodeSortedList: SortedList<Node> private val mNodeSortedList: SortedList<Node>
private val mInflater: LayoutInflater = LayoutInflater.from(context) private val mInflater: LayoutInflater = LayoutInflater.from(context)
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F private var mSubtextDefaultDimension: Float = 0F
@@ -100,9 +100,7 @@ class NodeAdapter (private val context: Context)
this.mDatabase = Database.getInstance() this.mDatabase = Database.getInstance()
// Color of content selection // Color of content selection
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
taContentSelectionColor.recycle()
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
@@ -305,7 +303,7 @@ class NodeAdapter (private val context: Context)
} }
holder.imageIdentifier?.setColorFilter(iconColor) holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply { holder.icon.apply {
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor) mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon // Relative size of the icon
layoutParams?.apply { layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()

View File

@@ -36,7 +36,6 @@ import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
@@ -81,10 +80,9 @@ class SearchEntryCursorAdapter(private val context: Context,
val viewHolder = view.tag as ViewHolder val viewHolder = view.tag as ViewHolder
// Assign image // Assign image
viewHolder.imageViewIcon?.assignDatabaseIcon( viewHolder.imageViewIcon?.let { iconView ->
database.drawFactory, database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
currentEntry.icon, }
iconColor)
// Assign title // Assign title
viewHolder.textViewTitle?.apply { viewHolder.textViewTitle?.apply {
@@ -110,10 +108,24 @@ class SearchEntryCursorAdapter(private val context: Context,
return database.createEntry()?.apply { return database.createEntry()?.apply {
database.startManageEntry(this) database.startManageEntry(this)
entryKDB?.let { entryKDB -> entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory) (cursor as EntryCursorKDB).populateEntry(entryKDB,
{ standardIconId ->
database.getStandardIcon(standardIconId)
},
{ customIconId ->
database.getCustomIcon(customIconId)
}
)
} }
entryKDBX?.let { entryKDBX -> entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory) (cursor as EntryCursorKDBX).populateEntry(entryKDBX,
{ standardIconId ->
database.getStandardIcon(standardIconId)
},
{ customIconId ->
database.getCustomIcon(customIconId)
}
)
} }
database.stopManageEntry(this) database.stopManageEntry(this)
} }

View File

@@ -25,7 +25,7 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.IBinder import android.os.IBinder
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
import java.util.* import java.util.*

View File

@@ -46,8 +46,6 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
@@ -86,6 +84,24 @@ object AutofillHelper {
return "" return ""
} }
private fun newRemoteViews(context: Context,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
try {
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
}
return presentation
}
private fun buildDataset(context: Context, private fun buildDataset(context: Context,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result, struct: StructureParser.Result,
@@ -116,6 +132,21 @@ object AutofillHelper {
} }
} }
/**
* Method to assign a drawable to a new icon from a database icon
*/
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
try {
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
@@ -267,26 +298,4 @@ object AutofillHelper {
activity.finish() activity.finish()
} }
} }
private fun newRemoteViews(context: Context,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
presentation.assignDatabaseIcon(context,
R.id.autofill_entry_icon,
Database.getInstance().drawFactory,
remoteViewsIcon,
ContextCompat.getColor(context, R.color.green))
}
return presentation
}
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
return createIconFromDatabaseIcon(context,
Database.getInstance().drawFactory,
entryInfo.icon,
ContextCompat.getColor(context, R.color.green))
}
} }

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.graphics.BlendMode import android.graphics.BlendMode
@@ -130,6 +131,7 @@ class KeeAutofillService : AutofillService() {
) )
} }
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView

View File

@@ -24,39 +24,26 @@ import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
open class AssignPasswordInDatabaseRunnable ( open class AssignPasswordInDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
protected val mDatabaseUri: Uri, protected val mDatabaseUri: Uri,
withMasterPassword: Boolean, protected val mMainCredential: MainCredential)
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?)
: SaveDatabaseRunnable(context, database, true) { : SaveDatabaseRunnable(context, database, true) {
private var mMasterPassword: String? = null
protected var mKeyFileUri: Uri? = null
private var mBackupKey: ByteArray? = null private var mBackupKey: ByteArray? = null
init {
if (withMasterPassword)
this.mMasterPassword = masterPassword
if (withKeyFile)
this.mKeyFileUri = keyFile
}
override fun onStartRun() { override fun onStartRun() {
// Set key // Set key
try { try {
// TODO move master key methods
mBackupKey = ByteArray(database.masterKey.size) mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri) val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.retrieveMasterKey(mMasterPassword, uriInputStream) database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) { } catch (e: Exception) {
erase(mBackupKey) erase(mBackupKey)
setError(e) setError(e)

View File

@@ -24,6 +24,7 @@ import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -32,12 +33,9 @@ class CreateDatabaseRunnable(context: Context,
databaseUri: Uri, databaseUri: Uri,
private val databaseName: String, private val databaseName: String,
private val rootName: String, private val rootName: String,
withMasterPassword: Boolean, mainCredential: MainCredential,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) { : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
override fun onStartRun() { override fun onStartRun() {
try { try {
@@ -61,7 +59,7 @@ class CreateDatabaseRunnable(context: Context,
if (PreferencesUtil.rememberDatabaseLocations(context)) { if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext) FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, .addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null) if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
} }
// Register the current time to init the lock timer // Register the current time to init the lock timer

View File

@@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.UriUtil
class LoadDatabaseRunnable(private val context: Context, class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database, private val mDatabase: Database,
private val mUri: Uri, private val mUri: Uri,
private val mPass: String?, private val mMainCredential: MainCredential,
private val mKey: Uri?,
private val mReadonly: Boolean, private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?, private val mCipherEntity: CipherDatabaseEntity?,
private val mFixDuplicateUUID: Boolean, private val mFixDuplicateUUID: Boolean,
@@ -51,10 +50,12 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.loadData(mUri, mPass, mKey, mDatabase.loadData(mUri,
mMainCredential,
mReadonly, mReadonly,
context.contentResolver, context.contentResolver,
UriUtil.getBinaryDir(context), UriUtil.getBinaryDir(context),
Database.LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID, mFixDuplicateUUID,
progressTaskUpdater) progressTaskUpdater)
} }
@@ -67,7 +68,7 @@ class LoadDatabaseRunnable(private val context: Context,
if (PreferencesUtil.rememberDatabaseLocations(context)) { if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context) FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, .addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null) if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
} }
// Register the biometric // Register the biometric

View File

@@ -37,36 +37,37 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
@@ -264,30 +265,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
*/ */
fun startDatabaseCreate(databaseUri: Uri, fun startDatabaseCreate(databaseUri: Uri,
masterPasswordChecked: Boolean, mainCredential: MainCredential) {
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
} }
, ACTION_DATABASE_CREATE_TASK) , ACTION_DATABASE_CREATE_TASK)
} }
fun startDatabaseLoad(databaseUri: Uri, fun startDatabaseLoad(databaseUri: Uri,
masterPassword: String?, mainCredential: MainCredential,
keyFile: Uri?,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEntity: CipherDatabaseEntity?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -303,17 +296,11 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
} }
fun startDatabaseAssignPassword(databaseUri: Uri, fun startDatabaseAssignPassword(databaseUri: Uri,
masterPasswordChecked: Boolean, mainCredential: MainCredential) {
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
} }
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK) , ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
} }

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -34,18 +33,22 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() { : ActionRunnable() {
private var tempCipherKey: Database.LoadedKey? = null
override fun onStartRun() { override fun onStartRun() {
tempCipherKey = mDatabase.loadedCipherKey
// Clear before we load // Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context)) mDatabase.clear(UriUtil.getBinaryDir(context))
mDatabase.wasReloaded = true
} }
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.reloadData(context.contentResolver, mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context), UriUtil.getBinaryDir(context),
tempCipherKey ?: Database.LoadedKey.generateNewCipherKey(),
progressTaskUpdater) progressTaskUpdater)
} } catch (e: LoadDatabaseException) {
catch (e: LoadDatabaseException) {
setError(e) setError(e)
} }
@@ -53,6 +56,7 @@ class ReloadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
tempCipherKey = null
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
} }
} }

View File

@@ -52,16 +52,9 @@ class CopyNodesRunnable constructor(
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) { if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
// Update entry with new values // Update entry with new values
mNewParent.touch(modified = false, touchParents = true) mNewParent.touch(modified = false, touchParents = true)
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent) val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
if (entryCopied != null) { entryCopied.touch(modified = true, touchParents = true)
entryCopied.touch(modified = true, touchParents = true) mEntriesCopied.add(entryCopied)
mEntriesCopied.add(entryCopied)
} else {
Log.e(TAG, "Unable to create a copy of the entry")
setError(CopyEntryDatabaseException())
break@foreachNode
}
} else { } else {
// Only finish thread // Only finish thread
setError(CopyEntryDatabaseException()) setError(CopyEntryDatabaseException())

View File

@@ -65,7 +65,7 @@ class DeleteNodesRunnable(context: Context,
database.deleteEntry(currentNode) database.deleteEntry(currentNode)
} }
// Remove the oldest attachments // Remove the oldest attachments
currentNode.getAttachments(database.binaryPool).forEach { currentNode.getAttachments(database.attachmentPool).forEach {
database.removeAttachmentIfNotUsed(it) database.removeAttachmentIfNotUsed(it)
} }
} }

View File

@@ -42,14 +42,14 @@ class UpdateEntryRunnable constructor(
mNewEntry.addParentFrom(mOldEntry) mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments // Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true) val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true) val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments) val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name // Not use equals because only check name
newEntryAttachments.forEach { newAttachment -> newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment -> oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment) && oldAttachment.binaryData == newAttachment.binaryData)
attachmentsToRemove.remove(oldAttachment) attachmentsToRemove.remove(oldAttachment)
} }
} }
@@ -60,7 +60,7 @@ class UpdateEntryRunnable constructor(
// Create an entry history (an entry history don't have history) // Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false)) mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry, database.binaryPool) database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
// Only change data in index // Only change data in index
database.updateEntry(mOldEntry) database.updateEntry(mOldEntry)

View File

@@ -23,7 +23,9 @@ import android.database.MatrixCursor
import android.provider.BaseColumns import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import java.util.* import java.util.*
@@ -49,12 +51,16 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
abstract fun getPwNodeId(): NodeId<EntryId> abstract fun getPwNodeId(): NodeId<EntryId>
open fun populateEntry(pwEntry: PwEntryV, iconFactory: IconImageFactory) { open fun populateEntry(pwEntry: PwEntryV,
retrieveStandardIcon: (Int) -> IconImageStandard,
retrieveCustomIcon: (UUID) -> IconImageCustom) {
pwEntry.nodeId = getPwNodeId() pwEntry.nodeId = getPwNodeId()
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE)) pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD))) val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
pwEntry.icon = iconStandard val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
pwEntry.icon = IconImage(iconStandard, iconCustom)
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME)) pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD)) pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.database.cursor package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() { class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
@@ -30,9 +29,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
entry.id.mostSignificantBits, entry.id.mostSignificantBits,
entry.id.leastSignificantBits, entry.id.leastSignificantBits,
entry.title, entry.title,
entry.icon.iconId, entry.icon.standard.id,
DatabaseVersioned.UUID_ZERO.mostSignificantBits, entry.icon.custom.uuid.mostSignificantBits,
DatabaseVersioned.UUID_ZERO.leastSignificantBits, entry.icon.custom.uuid.leastSignificantBits,
entry.username, entry.username,
entry.password, entry.password,
entry.url, entry.url,

View File

@@ -20,9 +20,9 @@
package com.kunzisoft.keepass.database.cursor package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.UUID import java.util.*
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() { class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
@@ -34,9 +34,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entry.id.mostSignificantBits, entry.id.mostSignificantBits,
entry.id.leastSignificantBits, entry.id.leastSignificantBits,
entry.title, entry.title,
entry.icon.iconId, entry.icon.standard.id,
entry.iconCustom.uuid.mostSignificantBits, entry.icon.custom.uuid.mostSignificantBits,
entry.iconCustom.uuid.leastSignificantBits, entry.icon.custom.uuid.leastSignificantBits,
entry.username, entry.username,
entry.password, entry.password,
entry.url, entry.url,
@@ -52,14 +52,10 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entryId++ entryId++
} }
override fun populateEntry(pwEntry: EntryKDBX, iconFactory: IconImageFactory) { override fun populateEntry(pwEntry: EntryKDBX,
super.populateEntry(pwEntry, iconFactory) retrieveStandardIcon: (Int) -> IconImageStandard,
retrieveCustomIcon: (UUID) -> IconImageCustom) {
// Retrieve custom icon super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
val iconCustom = iconFactory.getIcon(
UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
pwEntry.iconCustom = iconCustom
// Retrieve extra fields // Retrieve extra fields
if (extraFieldCursor.moveToFirst()) { if (extraFieldCursor.moveToFirst()) {

View File

@@ -21,19 +21,21 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryByte
import com.kunzisoft.keepass.database.element.database.BinaryData
data class Attachment(var name: String, data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable { var binaryData: BinaryData) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString() ?: "", parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment() parcel.readParcelable(BinaryData::class.java.classLoader) ?: BinaryByte()
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name) parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags) parcel.writeParcelable(binaryData, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {
@@ -41,7 +43,7 @@ data class Attachment(var name: String,
} }
override fun toString(): String { override fun toString(): String {
return "$name at $binaryAttachment" return "$name at $binaryData"
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -26,7 +26,9 @@ import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.database.* import com.kunzisoft.keepass.database.element.database.*
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -41,12 +43,16 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.stream.readBytes4ToUInt import com.kunzisoft.keepass.stream.readBytes4ToUInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import java.io.* import java.io.*
import java.security.Key
import java.security.SecureRandom
import java.util.* import java.util.*
import javax.crypto.KeyGenerator
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -63,7 +69,10 @@ class Database {
var isReadOnly = false var isReadOnly = false
val drawFactory = IconDrawableFactory() val iconDrawableFactory = IconDrawableFactory(
{ loadedCipherKey },
{ iconId -> iconsManager.getBinaryForCustomIcon(iconId) }
)
var loaded = false var loaded = false
set(value) { set(value) {
@@ -71,13 +80,64 @@ class Database {
loadTimestamp = if (field) System.currentTimeMillis() else null loadTimestamp = if (field) System.currentTimeMillis() else null
} }
/**
* To reload the main activity
*/
var wasReloaded = false
var loadTimestamp: Long? = null var loadTimestamp: Long? = null
private set private set
val iconFactory: IconImageFactory /**
get() { * Cipher key regenerated when the database is loaded and closed
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory() * Can be used to temporarily store database elements
*/
var loadedCipherKey: LoadedKey?
private set(value) {
mDatabaseKDB?.loadedCipherKey = value
mDatabaseKDBX?.loadedCipherKey = value
} }
get() {
return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey
}
private val iconsManager: IconsManager
get() {
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
}
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
return iconsManager.doForEachStandardIcon(action)
}
fun getStandardIcon(iconId: Int): IconImageStandard {
return iconsManager.getIcon(iconId)
}
val allowCustomIcons: Boolean
get() = mDatabaseKDBX != null
fun doForEachCustomIcons(action: (IconImageCustom, BinaryData) -> Unit) {
return iconsManager.doForEachCustomIcon(action)
}
fun getCustomIcon(iconId: UUID): IconImageCustom {
return iconsManager.getIcon(iconId)
}
fun buildNewCustomIcon(cacheDirectory: File,
result: (IconImageCustom?, BinaryData?) -> Unit) {
mDatabaseKDBX?.buildNewCustomIcon(cacheDirectory, null, result)
}
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return mDatabaseKDBX?.isCustomIconBinaryDuplicate(binaryData) ?: false
}
fun removeCustomIcon(customIcon: IconImageCustom) {
iconDrawableFactory.clearFromCache(customIcon)
iconsManager.removeCustomIcon(customIcon.uuid)
}
val allowName: Boolean val allowName: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -320,12 +380,26 @@ class Database {
} }
fun createData(databaseUri: Uri, databaseName: String, rootName: String) { fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName)) val newDatabase = DatabaseKDBX(databaseName, rootName)
newDatabase.loadedCipherKey = LoadedKey.generateNewCipherKey()
setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
this.loaded = true this.loaded = true
} }
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
companion object {
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
fun generateNewCipherKey(): LoadedKey {
val iv = ByteArray(8)
SecureRandom().nextBytes(iv)
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
}
}
}
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB, openDatabaseKDB: (InputStream) -> DatabaseKDB,
@@ -366,16 +440,20 @@ class Database {
loaded = true loaded = true
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
throw e throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally { } finally {
databaseInputStream?.close() databaseInputStream?.close()
} }
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
fun loadData(uri: Uri, password: String?, keyfile: Uri?, fun loadData(uri: Uri,
mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
contentResolver: ContentResolver, contentResolver: ContentResolver,
cacheDirectory: File, cacheDirectory: File,
tempCipherKey: LoadedKey,
fixDuplicateUUID: Boolean, fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
@@ -389,8 +467,8 @@ class Database {
var keyFileInputStream: InputStream? = null var keyFileInputStream: InputStream? = null
try { try {
// Get keyFile inputStream // Get keyFile inputStream
keyfile?.let { mainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
} }
// Read database stream for the first time // Read database stream for the first time
@@ -398,16 +476,18 @@ class Database {
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDB(cacheDirectory) DatabaseInputKDB(cacheDirectory)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
password, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
tempCipherKey,
progressTaskUpdater, progressTaskUpdater,
fixDuplicateUUID) fixDuplicateUUID)
}, },
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDBX(cacheDirectory) DatabaseInputKDBX(cacheDirectory)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
password, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
tempCipherKey,
progressTaskUpdater, progressTaskUpdater,
fixDuplicateUUID) fixDuplicateUUID)
} }
@@ -427,27 +507,39 @@ class Database {
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver, fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File, cacheDirectory: File,
tempCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI // Retrieve the stream from the old database URI
fileUri?.let { oldDatabaseUri -> try {
readDatabaseStream(contentResolver, oldDatabaseUri, fileUri?.let { oldDatabaseUri ->
{ databaseInputStream -> readDatabaseStream(contentResolver, oldDatabaseUri,
DatabaseInputKDB(cacheDirectory) { databaseInputStream ->
.openDatabase(databaseInputStream, DatabaseInputKDB(cacheDirectory)
masterKey, .openDatabase(databaseInputStream,
progressTaskUpdater) masterKey,
}, tempCipherKey,
{ databaseInputStream -> progressTaskUpdater)
DatabaseInputKDBX(cacheDirectory) },
.openDatabase(databaseInputStream, { databaseInputStream ->
masterKey, DatabaseInputKDBX(cacheDirectory)
progressTaskUpdater) .openDatabase(databaseInputStream,
} masterKey,
) tempCipherKey,
} ?: run { progressTaskUpdater)
Log.e(TAG, "Database URI is null, database cannot be reloaded") }
throw IODatabaseException() )
} ?: run {
Log.e(TAG, "Database URI is null, database cannot be reloaded")
throw IODatabaseException()
}
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e)
throw FileNotFoundDatabaseException()
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} }
} }
@@ -482,9 +574,10 @@ class Database {
}, omitBackup, max) }, omitBackup, max)
} }
val binaryPool: BinaryPool val attachmentPool: AttachmentPool
get() { get() {
return mDatabaseKDBX?.binaryPool ?: BinaryPool() // Binary pool is functionally only in KDBX
return mDatabaseKDBX?.binaryPool ?: AttachmentPool()
} }
val allowMultipleAttachments: Boolean val allowMultipleAttachments: Boolean
@@ -496,17 +589,17 @@ class Database {
return false return false
} }
fun buildNewBinary(cacheDirectory: File, fun buildNewBinaryAttachment(cacheDirectory: File,
compressed: Boolean = false, compressed: Boolean = false,
protected: Boolean = false): BinaryAttachment? { protected: Boolean = false): BinaryData? {
return mDatabaseKDB?.buildNewBinary(cacheDirectory) return mDatabaseKDB?.buildNewAttachment(cacheDirectory)
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected) ?: mDatabaseKDBX?.buildNewAttachment(cacheDirectory, compressed, protected)
} }
fun removeAttachmentIfNotUsed(attachment: Attachment) { fun removeAttachmentIfNotUsed(attachment: Attachment) {
// No need in KDB database because unique attachment by entry // No need in KDB database because unique attachment by entry
// Don't clear to fix upload multiple times // Don't clear to fix upload multiple times
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false) mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryData, false)
} }
fun removeUnlinkedAttachments() { fun removeUnlinkedAttachments() {
@@ -575,7 +668,8 @@ class Database {
} }
fun clear(filesDirectory: File? = null) { fun clear(filesDirectory: File? = null) {
drawFactory.clearCache() iconsManager.clearCache()
iconDrawableFactory.clearCache()
// Delete the cache of the database if present // Delete the cache of the database if present
mDatabaseKDB?.clearCache() mDatabaseKDB?.clearCache()
mDatabaseKDBX?.clearCache() mDatabaseKDBX?.clearCache()
@@ -608,7 +702,9 @@ class Database {
} }
} }
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
val password = mainCredential.masterPassword
val containsKeyFile = mainCredential.keyFileUri != null
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile) return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile) ?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false ?: false
@@ -739,7 +835,7 @@ class Database {
* @param entryToCopy * @param entryToCopy
* @param newParent * @param newParent
*/ */
fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry? { fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry {
val entryCopied = Entry(entryToCopy, false) val entryCopied = Entry(entryToCopy, false)
entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID() entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID()
entryCopied.parent = newParent entryCopied.parent = newParent
@@ -896,7 +992,7 @@ class Database {
rootGroup?.doForEachChildAndForIt( rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() { object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node, binaryPool) removeOldestEntryHistory(node, attachmentPool)
return true return true
} }
}, },
@@ -911,7 +1007,7 @@ class Database {
/** /**
* Remove oldest history if more than max items or max memory * Remove oldest history if more than max items or max memory
*/ */
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) { fun removeOldestEntryHistory(entry: Entry, attachmentPool: AttachmentPool) {
mDatabaseKDBX?.let { mDatabaseKDBX?.let {
val maxItems = historyMaxItems val maxItems = historyMaxItems
if (maxItems >= 0) { if (maxItems >= 0) {
@@ -925,7 +1021,7 @@ class Database {
while (true) { while (true) {
var historySize: Long = 0 var historySize: Long = 0
for (entryHistory in entry.getHistory()) { for (entryHistory in entry.getHistory()) {
historySize += entryHistory.getSize(binaryPool) historySize += entryHistory.getSize(attachmentPool)
} }
if (historySize > maxSize) { if (historySize > maxSize) {
removeOldestEntryHistory(entry) removeOldestEntryHistory(entry)
@@ -939,7 +1035,7 @@ class Database {
private fun removeOldestEntryHistory(entry: Entry) { private fun removeOldestEntryHistory(entry: Entry) {
entry.removeOldestEntryFromHistory()?.let { entry.removeOldestEntryFromHistory()?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove -> it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove) removeAttachmentIfNotUsed(attachmentToRemove)
} }
} }
@@ -947,7 +1043,7 @@ class Database {
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) { fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
entry.removeEntryFromHistory(entryHistoryPosition)?.let { entry.removeEntryFromHistory(entryHistoryPosition)?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove -> it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove) removeAttachmentIfNotUsed(attachmentToRemove)
} }
} }

View File

@@ -21,14 +21,12 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryPool import com.kunzisoft.keepass.database.element.database.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -109,7 +107,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
override var icon: IconImage override var icon: IconImage
get() { get() {
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImageStandard() return entryKDB?.icon ?: entryKDBX?.icon ?: IconImage()
} }
set(value) { set(value) {
entryKDB?.icon = value entryKDB?.icon = value
@@ -257,31 +255,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
} }
} }
/*
------------
KDB Methods
------------
*/
/**
* If it's a node with only meta information like Meta-info SYSTEM Database Color
* @return false by default, true if it's a meta stream
*/
val isMetaStream: Boolean
get() = entryKDB?.isMetaStream ?: false
/* /*
------------ ------------
KDBX Methods KDBX Methods
------------ ------------
*/ */
var iconCustom: IconImageCustom
get() = entryKDBX?.iconCustom ?: IconImageCustom.UNKNOWN_ICON
set(value) {
entryKDBX?.iconCustom = value
}
/** /**
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not) * Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
* @return Map of label/value * @return Map of label/value
@@ -330,12 +309,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.stopToManageFieldReferences() entryKDBX?.stopToManageFieldReferences()
} }
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> { fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>() val attachments = ArrayList<Attachment>()
entryKDB?.getAttachment()?.let { entryKDB?.getAttachment()?.let {
attachments.add(it) attachments.add(it)
} }
entryKDBX?.getAttachments(binaryPool, inHistory)?.let { entryKDBX?.getAttachments(attachmentPool, inHistory)?.let {
attachments.addAll(it) attachments.addAll(it)
} }
return attachments return attachments
@@ -346,12 +325,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|| entryKDBX?.containsAttachment() == true || entryKDBX?.containsAttachment() == true
} }
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
attachments.forEach {
putAttachment(it, binaryPool)
}
}
private fun removeAttachment(attachment: Attachment) { private fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment) entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment) entryKDBX?.removeAttachment(attachment)
@@ -362,9 +335,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.removeAttachments() entryKDBX?.removeAttachments()
} }
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
entryKDB?.putAttachment(attachment) entryKDB?.putAttachment(attachment)
entryKDBX?.putAttachment(attachment, binaryPool) entryKDBX?.putAttachment(attachment, attachmentPool)
} }
fun getHistory(): ArrayList<Entry> { fun getHistory(): ArrayList<Entry> {
@@ -396,8 +369,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
return null return null
} }
fun getSize(binaryPool: BinaryPool): Long { fun getSize(attachmentPool: AttachmentPool): Long {
return entryKDBX?.getSize(binaryPool) ?: 0L return entryKDBX?.getSize(attachmentPool) ?: 0L
} }
fun containsCustomData(): Boolean { fun containsCustomData(): Boolean {
@@ -427,7 +400,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.username = username entryInfo.username = username
entryInfo.password = password entryInfo.password = password
entryInfo.creationTime = creationTime entryInfo.creationTime = creationTime
entryInfo.modificationTime = lastModificationTime entryInfo.lastModificationTime = lastModificationTime
entryInfo.expires = expires entryInfo.expires = expires
entryInfo.expiryTime = expiryTime entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
@@ -439,7 +412,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
// Replace parameter fields by generated OTP fields // Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
} }
database?.binaryPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool) entryInfo.attachments = getAttachments(binaryPool)
} }
@@ -466,8 +439,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
url = newEntryInfo.url url = newEntryInfo.url
notes = newEntryInfo.notes notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments) newEntryInfo.attachments.forEach { attachment ->
putAttachment(attachment, binaryPool)
}
} }
database?.stopManageEntry(this) database?.stopManageEntry(this)

View File

@@ -26,9 +26,9 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.* import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -40,6 +40,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
var groupKDBX: GroupKDBX? = null var groupKDBX: GroupKDBX? = null
private set private set
// Virtual group is used to defined a detached database group
var isVirtual = false
fun updateWith(group: Group) { fun updateWith(group: Group) {
group.groupKDB?.let { group.groupKDB?.let {
this.groupKDB?.updateWith(it) this.groupKDB?.updateWith(it)
@@ -77,6 +80,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader) groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader) groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
isVirtual = parcel.readByte().toInt() != 0
} }
enum class ChildFilter { enum class ChildFilter {
@@ -110,6 +114,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(groupKDB, flags) dest.writeParcelable(groupKDB, flags)
dest.writeParcelable(groupKDBX, flags) dest.writeParcelable(groupKDBX, flags)
dest.writeByte((if (isVirtual) 1 else 0).toByte())
} }
override val nodeId: NodeId<*>? override val nodeId: NodeId<*>?
@@ -123,7 +128,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
override var icon: IconImage override var icon: IconImage
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImageStandard() get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImage()
set(value) { set(value) {
groupKDB?.icon = value groupKDB?.icon = value
groupKDBX?.icon = value groupKDBX?.icon = value
@@ -232,6 +237,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
override val isCurrentlyExpires: Boolean override val isCurrentlyExpires: Boolean
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
var notes: String?
get() = groupKDBX?.notes
set(value) {
value?.let {
groupKDBX?.notes = it
}
}
override fun getChildGroups(): List<Group> { override fun getChildGroups(): List<Group> {
return groupKDB?.getChildGroups()?.map { return groupKDB?.getChildGroups()?.map {
Group(it) Group(it)
@@ -335,9 +348,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.removeChildren() groupKDBX?.removeChildren()
} }
override fun allowAddEntryIfIsRoot(): Boolean { val allowAddEntryIfIsRoot: Boolean
return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false get() = groupKDBX != null
}
val allowAddNoteInGroup: Boolean
get() = groupKDBX != null
/* /*
------------ ------------
@@ -391,6 +406,35 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
return groupKDBX?.containsCustomData() ?: false return groupKDBX?.containsCustomData() ?: false
} }
/*
------------
Converter
------------
*/
fun getGroupInfo(): GroupInfo {
val groupInfo = GroupInfo()
groupInfo.title = title
groupInfo.icon = icon
groupInfo.creationTime = creationTime
groupInfo.lastModificationTime = lastModificationTime
groupInfo.expires = expires
groupInfo.expiryTime = expiryTime
groupInfo.notes = notes
return groupInfo
}
fun setGroupInfo(groupInfo: GroupInfo) {
title = groupInfo.title
icon = groupInfo.icon
// Update date time, creation time stay as is
lastModificationTime = DateInstant()
lastAccessTime = DateInstant()
expires = groupInfo.expires
expiryTime = groupInfo.expiryTime
notes = groupInfo.notes
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.database
class AttachmentPool : BinaryPool<Int>() {
/**
* Utility method to find an unused key in the pool
*/
override fun findUnusedKey(): Int {
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
/**
* To register a binary with a ref corresponding to an ordered index
*/
fun getBinaryIndexFromKey(key: Int): Int? {
val index = orderedBinariesWithoutDuplication().indexOfFirst { it.keys.contains(key) }
return if (index < 0)
null
else
index
}
}

View File

@@ -1,207 +0,0 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.database
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes
import java.io.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
class BinaryAttachment : Parcelable {
private var dataFile: File? = null
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
var isCorrupted: Boolean = false
fun length(): Long {
return dataFile?.length() ?: 0
}
/**
* Empty protected binary
*/
constructor()
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
this.dataFile = dataFile
this.isCompressed = compressed
this.isProtected = protected
}
private constructor(parcel: Parcel) {
parcel.readString()?.let {
dataFile = File(it)
}
isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
}
@Throws(IOException::class)
fun getInputDataStream(): InputStream {
return when {
length() > 0 -> FileInputStream(dataFile!!)
else -> ByteArrayInputStream(ByteArray(0))
}
}
@Throws(IOException::class)
fun getUnGzipInputDataStream(): InputStream {
return if (isCompressed)
GZIPInputStream(getInputDataStream())
else
getInputDataStream()
}
@Throws(IOException::class)
fun getOutputDataStream(): OutputStream {
return when {
dataFile != null -> FileOutputStream(dataFile!!)
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream())
} else {
getOutputDataStream()
}
}
@Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (!isCompressed) {
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
getInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
// Remove unGzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
}
}
}
@Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
if (isCompressed) {
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
FileOutputStream(fileBinaryDecompress).use { outputStream ->
getUnGzipInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
}
}
@Throws(IOException::class)
fun clear() {
if (dataFile != null && !dataFile!!.delete())
throw IOException("Unable to delete temp file " + dataFile!!.absolutePath)
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
if (other == null || javaClass != other.javaClass)
return false
if (other !is BinaryAttachment)
return false
var sameData = false
if (dataFile != null && dataFile == other.dataFile)
sameData = true
return isCompressed == other.isCompressed
&& isProtected == other.isProtected
&& isCorrupted == other.isCorrupted
&& sameData
}
override fun hashCode(): Int {
var result = 0
result = 31 * result + if (isCompressed) 1 else 0
result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode()
return result
}
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(dataFile?.absolutePath)
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
}
companion object {
private val TAG = BinaryAttachment::class.java.name
@JvmField
val CREATOR: Parcelable.Creator<BinaryAttachment> = object : Parcelable.Creator<BinaryAttachment> {
override fun createFromParcel(parcel: Parcel): BinaryAttachment {
return BinaryAttachment(parcel)
}
override fun newArray(size: Int): Array<BinaryAttachment?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.database
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.stream.readAllBytes
import java.io.*
import java.util.zip.GZIPOutputStream
class BinaryByte : BinaryData {
private var mDataByte: ByteArray = ByteArray(0)
/**
* Empty protected binary
*/
constructor() : super()
constructor(byteArray: ByteArray,
compressed: Boolean = false,
protected: Boolean = false) : super(compressed, protected) {
this.mDataByte = byteArray
}
constructor(parcel: Parcel) : super(parcel) {
val byteArray = ByteArray(parcel.readInt())
parcel.readByteArray(byteArray)
mDataByte = byteArray
}
@Throws(IOException::class)
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return ByteArrayInputStream(mDataByte)
}
@Throws(IOException::class)
override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return ByteOutputStream()
}
@Throws(IOException::class)
override fun compress(cipherKey: Database.LoadedKey) {
if (!isCompressed) {
GZIPOutputStream(getOutputDataStream(cipherKey)).use { outputStream ->
getInputDataStream(cipherKey).use { inputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
isCompressed = true
}
}
}
@Throws(IOException::class)
override fun decompress(cipherKey: Database.LoadedKey) {
if (isCompressed) {
getUnGzipInputDataStream(cipherKey).use { inputStream ->
getOutputDataStream(cipherKey).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
isCompressed = false
}
}
}
@Throws(IOException::class)
override fun clear() {
mDataByte = ByteArray(0)
}
override fun dataExists(): Boolean {
return mDataByte.isNotEmpty()
}
override fun getSize(): Long {
return mDataByte.size.toLong()
}
/**
* Hash of the raw encrypted file in temp folder, only to compare binary data
*/
override fun binaryHash(): Int {
return if (dataExists())
mDataByte.contentHashCode()
else
0
}
override fun toString(): String {
return mDataByte.toString()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeInt(mDataByte.size)
dest.writeByteArray(mDataByte)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BinaryByte) return false
if (!super.equals(other)) return false
if (!mDataByte.contentEquals(other.mDataByte)) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + mDataByte.contentHashCode()
return result
}
/**
* Custom OutputStream to calculate the size and hash of binary file
*/
private inner class ByteOutputStream : ByteArrayOutputStream() {
override fun close() {
mDataByte = this.toByteArray()
super.close()
}
}
companion object {
private val TAG = BinaryByte::class.java.name
const val MAX_BINARY_BYTES = 10240
@JvmField
val CREATOR: Parcelable.Creator<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
override fun createFromParcel(parcel: Parcel): BinaryByte {
return BinaryByte(parcel)
}
override fun newArray(size: Int): Array<BinaryByte?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.database
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Database
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
abstract class BinaryData : Parcelable {
var isCompressed: Boolean = false
protected set
var isProtected: Boolean = false
protected set
var isCorrupted: Boolean = false
/**
* Empty protected binary
*/
protected constructor()
protected constructor(compressed: Boolean = false, protected: Boolean = false) {
this.isCompressed = compressed
this.isProtected = protected
}
protected constructor(parcel: Parcel) {
isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
}
@Throws(IOException::class)
abstract fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream
@Throws(IOException::class)
abstract fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream
@Throws(IOException::class)
fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return if (isCompressed) {
GZIPInputStream(getInputDataStream(cipherKey))
} else {
getInputDataStream(cipherKey)
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream(cipherKey))
} else {
getOutputDataStream(cipherKey)
}
}
@Throws(IOException::class)
abstract fun compress(cipherKey: Database.LoadedKey)
@Throws(IOException::class)
abstract fun decompress(cipherKey: Database.LoadedKey)
@Throws(IOException::class)
abstract fun clear()
abstract fun dataExists(): Boolean
abstract fun getSize(): Long
abstract fun binaryHash(): Int
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BinaryData) return false
if (isCompressed != other.isCompressed) return false
if (isProtected != other.isProtected) return false
if (isCorrupted != other.isCorrupted) return false
return true
}
override fun hashCode(): Int {
var result = isCompressed.hashCode()
result = 31 * result + isProtected.hashCode()
result = 31 * result + isCorrupted.hashCode()
return result
}
companion object {
private val TAG = BinaryData::class.java.name
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.database
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.stream.readAllBytes
import org.apache.commons.io.output.CountingOutputStream
import java.io.*
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
class BinaryFile : BinaryData {
private var mDataFile: File? = null
private var mLength: Long = 0
private var mBinaryHash = 0
// Cipher to encrypt temp file
@Transient
private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
@Transient
private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
constructor() : super()
constructor(dataFile: File,
compressed: Boolean = false,
protected: Boolean = false) : super(compressed, protected) {
this.mDataFile = dataFile
this.mLength = 0
this.mBinaryHash = 0
}
constructor(parcel: Parcel) : super(parcel) {
parcel.readString()?.let {
mDataFile = File(it)
}
mLength = parcel.readLong()
mBinaryHash = parcel.readInt()
}
@Throws(IOException::class)
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return buildInputStream(mDataFile, cipherKey)
}
@Throws(IOException::class)
override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return buildOutputStream(mDataFile, cipherKey)
}
@Throws(IOException::class)
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
return when {
file != null && file.length() > 0 -> {
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
}
else -> ByteArrayInputStream(ByteArray(0))
}
}
@Throws(IOException::class)
private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
return when {
file != null -> {
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
}
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
override fun compress(cipherKey: Database.LoadedKey) {
mDataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (!isCompressed) {
// Encrypt the new gzipped temp file
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
getInputDataStream(cipherKey).use { inputStream ->
GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
// Remove ungzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
}
}
}
@Throws(IOException::class)
override fun decompress(cipherKey: Database.LoadedKey) {
mDataFile?.let { concreteDataFile ->
if (isCompressed) {
// Encrypt the new ungzipped temp file
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
getUnGzipInputDataStream(cipherKey).use { inputStream ->
buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
}
}
@Throws(IOException::class)
override fun clear() {
if (mDataFile != null && !mDataFile!!.delete())
throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath)
}
override fun dataExists(): Boolean {
return mDataFile != null && mLength > 0
}
override fun getSize(): Long {
return mLength
}
/**
* Hash of the raw encrypted file in temp folder, only to compare binary data
*/
@Throws(FileNotFoundException::class)
override fun binaryHash(): Int {
return mBinaryHash
}
override fun toString(): String {
return mDataFile.toString()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeString(mDataFile?.absolutePath)
dest.writeLong(mLength)
dest.writeInt(mBinaryHash)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BinaryFile) return false
if (!super.equals(other)) return false
return mDataFile != null && mDataFile == other.mDataFile
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (mDataFile?.hashCode() ?: 0)
result = 31 * result + mLength.hashCode()
result = 31 * result + mBinaryHash
return result
}
/**
* Custom OutputStream to calculate the size and hash of binary file
*/
private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
private val mMessageDigest: MessageDigest
init {
mLength = 0
mMessageDigest = MessageDigest.getInstance("MD5")
mBinaryHash = 0
}
override fun beforeWrite(n: Int) {
super.beforeWrite(n)
mLength = byteCount
}
override fun write(idx: Int) {
super.write(idx)
mMessageDigest.update(idx.toByte())
}
override fun write(bts: ByteArray) {
super.write(bts)
mMessageDigest.update(bts)
}
override fun write(bts: ByteArray, st: Int, end: Int) {
super.write(bts, st, end)
mMessageDigest.update(bts, st, end)
}
override fun close() {
super.close()
mLength = byteCount
val bytes = mMessageDigest.digest()
mBinaryHash = ByteBuffer.wrap(bytes).int
}
}
companion object {
private val TAG = BinaryFile::class.java.name
@JvmField
val CREATOR: Parcelable.Creator<BinaryFile> = object : Parcelable.Creator<BinaryFile> {
override fun createFromParcel(parcel: Parcel): BinaryFile {
return BinaryFile(parcel)
}
override fun newArray(size: Int): Array<BinaryFile?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -19,48 +19,78 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.util.Log
import java.io.File
import java.io.IOException import java.io.IOException
import kotlin.math.abs
class BinaryPool { abstract class BinaryPool<T> {
private val pool = LinkedHashMap<Int, BinaryAttachment>()
protected val pool = LinkedHashMap<T, BinaryData>()
// To build unique file id
private var creationId: String = System.currentTimeMillis().toString()
private var poolId: String = abs(javaClass.simpleName.hashCode()).toString()
private var binaryFileIncrement = 0L
/** /**
* To get a binary by the pool key (ref attribute in entry) * To get a binary by the pool key (ref attribute in entry)
*/ */
operator fun get(key: Int): BinaryAttachment? { operator fun get(key: T): BinaryData? {
return pool[key] return pool[key]
} }
/**
* Create and return a new binary file not yet linked to a binary
*/
fun put(key: T? = null,
builder: (uniqueBinaryId: String) -> BinaryData): KeyBinary<T> {
binaryFileIncrement++
val newBinaryFile: BinaryData = builder("$poolId$creationId$binaryFileIncrement")
val newKey = put(key, newBinaryFile)
return KeyBinary(newBinaryFile, newKey)
}
/** /**
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one * To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
*/ */
fun put(key: Int?, value: BinaryAttachment) { fun put(key: T?, value: BinaryData): T {
if (key == null) if (key == null)
put(value) return put(value)
else else
pool[key] = value pool[key] = value
return key
} }
/** /**
* To put a [binaryAttachment] in the pool, * To put a [binaryData] in the pool,
* if already exists, replace the current one, * if already exists, replace the current one,
* else add it with a new key * else add it with a new key
*/ */
fun put(binaryAttachment: BinaryAttachment): Int { fun put(binaryData: BinaryData): T {
var key = findKey(binaryAttachment) var key: T? = findKey(binaryData)
if (key == null) { if (key == null) {
key = findUnusedKey() key = findUnusedKey()
} }
pool[key] = binaryAttachment pool[key!!] = binaryData
return key return key
} }
/**
* Remove a binary from the pool with its [key], the file is not deleted
*/
@Throws(IOException::class)
fun remove(key: T) {
pool.remove(key)
// Don't clear attachment here because a file can be used in many BinaryAttachment
}
/** /**
* Remove a binary from the pool, the file is not deleted * Remove a binary from the pool, the file is not deleted
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun remove(binaryAttachment: BinaryAttachment) { fun remove(binaryData: BinaryData) {
findKey(binaryAttachment)?.let { findKey(binaryData)?.let {
pool.remove(it) pool.remove(it)
} }
// Don't clear attachment here because a file can be used in many BinaryAttachment // Don't clear attachment here because a file can be used in many BinaryAttachment
@@ -69,23 +99,18 @@ class BinaryPool {
/** /**
* Utility method to find an unused key in the pool * Utility method to find an unused key in the pool
*/ */
private fun findUnusedKey(): Int { abstract fun findUnusedKey(): T
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
/** /**
* Return key of [binaryAttachmentToRetrieve] or null if not found * Return key of [binaryDataToRetrieve] or null if not found
*/ */
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? { private fun findKey(binaryDataToRetrieve: BinaryData): T? {
val contains = pool.containsValue(binaryAttachmentToRetrieve) val contains = pool.containsValue(binaryDataToRetrieve)
return if (!contains) return if (!contains)
null null
else { else {
for ((key, binary) in pool) { for ((key, binary) in pool) {
if (binary == binaryAttachmentToRetrieve) { if (binary == binaryDataToRetrieve) {
return key return key
} }
} }
@@ -93,46 +118,116 @@ class BinaryPool {
} }
} }
fun isBinaryDuplicate(binaryData: BinaryData?): Boolean {
try {
binaryData?.let {
if (it.getSize() > 0) {
val searchBinaryMD5 = it.binaryHash()
var i = 0
for ((_, binary) in pool) {
if (binary.binaryHash() == searchBinaryMD5) {
i++
if (i > 1)
return true
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check binary duplication", e)
}
return false
}
/**
* To do an action on each binary in the pool (order is not important)
*/
private fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit,
condition: (key: T, binary: BinaryData) -> Boolean) {
for ((key, value) in pool) {
if (condition.invoke(key, value)) {
action.invoke(key, value)
}
}
}
fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit) {
doForEachBinary(action) { _, _ -> true }
}
/** /**
* Utility method to order binaries and solve index problem in database v4 * Utility method to order binaries and solve index problem in database v4
*/ */
private fun orderedBinaries(): List<KeyBinary> { protected fun orderedBinariesWithoutDuplication(condition: ((binary: BinaryData) -> Boolean) = { true })
val keyBinaryList = ArrayList<KeyBinary>() : List<KeyBinary<T>> {
val keyBinaryList = ArrayList<KeyBinary<T>>()
for ((key, binary) in pool) { for ((key, binary) in pool) {
keyBinaryList.add(KeyBinary(key, binary)) // Don't deduplicate
val existentBinary =
try {
if (binary.getSize() > 0) {
keyBinaryList.find {
val hash0 = it.binary.binaryHash()
val hash1 = binary.binaryHash()
hash0 != 0 && hash1 != 0 && hash0 == hash1
}
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check binary hash", e)
null
}
if (existentBinary == null) {
val newKeyBinary = KeyBinary(binary, key)
if (condition.invoke(newKeyBinary.binary)) {
keyBinaryList.add(newKeyBinary)
}
} else {
if (condition.invoke(existentBinary.binary)) {
existentBinary.addKey(key)
}
}
} }
return keyBinaryList return keyBinaryList
} }
/** /**
* To register a binary with a ref corresponding to an ordered index * Different from doForEach, provide an ordered index to each binary
*/ */
fun getBinaryIndexFromKey(key: Int): Int? { private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit,
val index = orderedBinaries().indexOfFirst { it.key == key } conditionToAdd: (binary: BinaryData) -> Boolean) {
return if (index < 0) orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
null action.invoke(keyBinary)
else }
index }
fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit) {
doForEachBinaryWithoutDuplication(action, { true })
} }
/** /**
* Different from doForEach, provide an ordered index to each binary * Different from doForEach, provide an ordered index to each binary
*/ */
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) { private fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit,
orderedBinaries().forEachIndexed(action) conditionToAdd: (binary: BinaryData) -> Boolean) {
orderedBinariesWithoutDuplication(conditionToAdd).forEachIndexed { index, keyBinary ->
action.invoke(index, keyBinary.binary)
}
} }
/** fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit) {
* To do an action on each binary in the pool doForEachOrderedBinaryWithoutDuplication(action, { true })
*/ }
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
pool.values.forEach { action.invoke(it) } fun isEmpty(): Boolean {
return pool.isEmpty()
} }
@Throws(IOException::class) @Throws(IOException::class)
fun clear() { fun clear() {
doForEachBinary { doForEachBinary { _, binary ->
it.clear() binary.clear()
} }
pool.clear() pool.clear()
} }
@@ -149,7 +244,20 @@ class BinaryPool {
} }
/** /**
* Utility data class to order binaries * Utility class to order binaries
*/ */
data class KeyBinary(val key: Int, val binary: BinaryAttachment) class KeyBinary<T>(val binary: BinaryData, key: T) {
val keys = HashSet<T>()
init {
addKey(key)
}
fun addKey(key: T) {
keys.add(key)
}
}
companion object {
private val TAG = BinaryPool::class.java.name
}
} }

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.database.element.database
import java.util.*
class CustomIconPool : BinaryPool<UUID>() {
override fun findUnusedKey(): UUID {
var newUUID = UUID.randomUUID()
while (pool.containsKey(newUUID)) {
newUUID = UUID.randomUUID()
}
return newUUID
}
}

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
@@ -44,7 +45,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var kdfListV3: MutableList<KdfEngine> = ArrayList() private var kdfListV3: MutableList<KdfEngine> = ArrayList()
private var binaryIncrement = 0 // Only to generate unique file name
private var binaryPool = AttachmentPool()
override val version: String override val version: String
get() = "KeePass 1" get() = "KeePass 1"
@@ -68,7 +70,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
getGroupById(backupGroupId) getGroupById(backupGroupId)
} }
override val kdfEngine: KdfEngine? override val kdfEngine: KdfEngine
get() = kdfListV3[0] get() = kdfListV3[0]
override val kdfAvailableList: List<KdfEngine> override val kdfAvailableList: List<KdfEngine>
@@ -175,6 +177,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return false return false
} }
override fun getStandardIcon(iconId: Int): IconImageStandard {
return this.iconsManager.getIcon(iconId)
}
override fun containsCustomData(): Boolean { override fun containsCustomData(): Boolean {
return false return false
} }
@@ -223,7 +229,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
// Create recycle bin // Create recycle bin
val recycleBinGroup = createGroup().apply { val recycleBinGroup = createGroup().apply {
title = BACKUP_FOLDER_TITLE title = BACKUP_FOLDER_TITLE
icon = iconFactory.trashIcon icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
} }
addGroupTo(recycleBinGroup, rootGroup) addGroupTo(recycleBinGroup, rootGroup)
backupGroupId = recycleBinGroup.id backupGroupId = recycleBinGroup.id
@@ -269,11 +275,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
addEntryTo(entry, origParent) addEntryTo(entry, origParent)
} }
fun buildNewBinary(cacheDirectory: File): BinaryAttachment { fun buildNewAttachment(cacheDirectory: File): BinaryData {
// Generate an unique new file // Generate an unique new file
val fileInCache = File(cacheDirectory, binaryIncrement.toString()) return binaryPool.put { uniqueBinaryId ->
binaryIncrement++ val fileInCache = File(cacheDirectory, uniqueBinaryId)
return BinaryAttachment(fileInCache) BinaryFile(fileInCache)
}.binary
} }
companion object { companion object {
@@ -281,7 +288,5 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
const val BACKUP_FOLDER_TITLE = "Backup" const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1 private const val BACKUP_FOLDER_UNDEFINED_ID = -1
const val BUFFER_SIZE_BYTES = 3 * 128
} }
} }

View File

@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BAC
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
@@ -105,11 +106,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastTopVisibleGroupUUID = UUID_ZERO var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig() var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>() val deletedObjects = ArrayList<DeletedObject>()
val customIcons = ArrayList<IconImageCustom>()
val customData = HashMap<String, String>() val customData = HashMap<String, String>()
var binaryPool = BinaryPool() var binaryPool = AttachmentPool()
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
var localizedAppName = "KeePassDX" var localizedAppName = "KeePassDX"
@@ -126,9 +125,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/ */
constructor(databaseName: String, rootName: String) { constructor(databaseName: String, rootName: String) {
name = databaseName name = databaseName
kdbxVersion = FILE_VERSION_32_3
val group = createGroup().apply { val group = createGroup().apply {
title = rootName title = rootName
icon = iconFactory.folderIcon icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
} }
rootGroup = group rootGroup = group
addGroupIndex(group) addGroupIndex(group)
@@ -210,10 +210,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
private fun compressAllBinaries() { private fun compressAllBinaries() {
binaryPool.doForEachBinary { binary -> binaryPool.doForEachBinary { _, binary ->
try { try {
val cipherKey = loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to compress binaries")
// To compress, create a new binary with file // To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES) binary.compress(cipherKey)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to compress $binary", e) Log.e(TAG, "Unable to compress $binary", e)
} }
@@ -221,9 +223,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
private fun decompressAllBinaries() { private fun decompressAllBinaries() {
binaryPool.doForEachBinary { binary -> binaryPool.doForEachBinary { _, binary ->
try { try {
binary.decompress(BUFFER_SIZE_BYTES) val cipherKey = loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to decompress binaries")
binary.decompress(cipherKey)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to decompress $binary", e) Log.e(TAG, "Unable to decompress $binary", e)
} }
@@ -302,16 +306,29 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
this.dataEngine = dataEngine this.dataEngine = dataEngine
} }
fun getCustomIcons(): List<IconImageCustom> { override fun getStandardIcon(iconId: Int): IconImageStandard {
return customIcons return this.iconsManager.getIcon(iconId)
} }
fun addCustomIcon(customIcon: IconImageCustom) { fun buildNewCustomIcon(cacheDirectory: File,
this.customIcons.add(customIcon) customIconId: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.buildNewCustomIcon(cacheDirectory, customIconId, result)
} }
fun getCustomData(): Map<String, String> { fun addCustomIcon(cacheDirectory: File,
return customData customIconId: UUID? = null,
dataSize: Int,
result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.addCustomIcon(cacheDirectory, customIconId, dataSize, result)
}
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
return iconsManager.isCustomIconBinaryDuplicate(binary)
}
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
return this.iconsManager.getIcon(iconUuid)
} }
fun putCustomData(label: String, value: String) { fun putCustomData(label: String, value: String) {
@@ -319,7 +336,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
override fun containsCustomData(): Boolean { override fun containsCustomData(): Boolean {
return getCustomData().isNotEmpty() return customData.isNotEmpty()
} }
@Throws(IOException::class) @Throws(IOException::class)
@@ -545,7 +562,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Create recycle bin // Create recycle bin
val recycleBinGroup = createGroup().apply { val recycleBinGroup = createGroup().apply {
title = resources.getString(R.string.recycle_bin) title = resources.getString(R.string.recycle_bin)
icon = iconFactory.trashIcon icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
enableAutoType = false enableAutoType = false
enableSearching = false enableSearching = false
isExpanded = false isExpanded = false
@@ -624,21 +641,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return publicCustomData.size() > 0 return publicCustomData.size() > 0
} }
fun buildNewBinary(cacheDirectory: File, fun buildNewAttachment(cacheDirectory: File,
compression: Boolean, compression: Boolean,
protection: Boolean, protection: Boolean,
binaryPoolId: Int? = null): BinaryAttachment { binaryPoolId: Int? = null): BinaryData {
// New file with current time return binaryPool.put(binaryPoolId) { uniqueBinaryId ->
val fileInCache = File(cacheDirectory, binaryIncrement.toString()) val fileInCache = File(cacheDirectory, uniqueBinaryId)
binaryIncrement++ BinaryFile(fileInCache, compression, protection)
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection) }.binary
// add attachment to pool
binaryPool.put(binaryPoolId, binaryAttachment)
return binaryAttachment
} }
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) { fun removeUnlinkedAttachment(binary: BinaryData, clear: Boolean) {
val listBinaries = ArrayList<BinaryAttachment>() val listBinaries = ArrayList<BinaryData>()
listBinaries.add(binary) listBinaries.add(binary)
removeUnlinkedAttachments(listBinaries, clear) removeUnlinkedAttachments(listBinaries, clear)
} }
@@ -647,11 +661,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
removeUnlinkedAttachments(emptyList(), clear) removeUnlinkedAttachments(emptyList(), clear)
} }
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) { private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
// Build binaries to remove with all binaries known // Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryAttachment>() val binariesToRemove = ArrayList<BinaryData>()
if (binaries.isEmpty()) { if (binaries.isEmpty()) {
binaryPool.doForEachBinary { binary -> binaryPool.doForEachBinary { _, binary ->
binariesToRemove.add(binary) binariesToRemove.add(binary)
} }
} else { } else {
@@ -661,7 +675,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() { rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean { override fun operate(node: EntryKDBX): Boolean {
node.getAttachments(binaryPool, true).forEach { node.getAttachments(binaryPool, true).forEach {
binariesToRemove.remove(it.binaryAttachment) binariesToRemove.remove(it.binaryData)
} }
return binariesToRemove.isNotEmpty() return binariesToRemove.isNotEmpty()
} }
@@ -708,7 +722,5 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val XML_ATTRIBUTE_DATA_HASH = "Hash" private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP const val BASE_64_FLAG = Base64.NO_WRAP
const val BUFFER_SIZE_BYTES = 3 * 128
} }
} }

View File

@@ -20,9 +20,11 @@
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.group.GroupVersioned import com.kunzisoft.keepass.database.element.group.GroupVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
@@ -54,8 +56,13 @@ abstract class DatabaseVersioned<
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
var iconFactory = IconImageFactory() /**
protected set * Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var loadedCipherKey: Database.LoadedKey? = null
val iconsManager = IconsManager()
var changeDuplicateId = false var changeDuplicateId = false
@@ -84,13 +91,13 @@ abstract class DatabaseVersioned<
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(IOException::class) @Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
masterKey = getMasterKey(key, keyInputStream) masterKey = getMasterKey(key, keyfileInputStream)
} }
@Throws(IOException::class) @Throws(IOException::class)
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray { protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyInputStream) val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key) val passwordKey = getPasswordKey(key)
val messageDigest: MessageDigest val messageDigest: MessageDigest
@@ -328,6 +335,8 @@ abstract class DatabaseVersioned<
abstract fun rootCanContainsEntry(): Boolean abstract fun rootCanContainsEntry(): Boolean
abstract fun getStandardIcon(iconId: Int): IconImageStandard
abstract fun containsCustomData(): Boolean abstract fun containsCustomData(): Boolean
fun addGroupTo(newGroup: Group, parent: Group?) { fun addGroupTo(newGroup: Group, parent: Group?) {

View File

@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
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.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
* Structure containing information about one entry. * Structure containing information about one entry.
@@ -56,7 +56,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
/** A string describing what is in binaryData */ /** A string describing what is in binaryData */
var binaryDescription = "" var binaryDescription = ""
var binaryData: BinaryAttachment? = null var binaryData: BinaryData? = null
// Determine if this is a MetaStream entry // Determine if this is a MetaStream entry
val isMetaStream: Boolean val isMetaStream: Boolean
@@ -68,7 +68,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
if (username.isEmpty()) return false if (username.isEmpty()) return false
if (username != PMS_ID_USER) return false if (username != PMS_ID_USER) return false
if (url.isEmpty()) return false if (url.isEmpty()) return false
return if (url != PMS_ID_URL) false else icon.isMetaStreamIcon if (url != PMS_ID_URL) return false
return icon.standard.id == KEY_ID
} }
override fun initNodeId(): NodeId<UUID> { override fun initNodeId(): NodeId<UUID> {
@@ -88,7 +89,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
url = parcel.readString() ?: url url = parcel.readString() ?: url
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
binaryDescription = parcel.readString() ?: binaryDescription binaryDescription = parcel.readString() ?: binaryDescription
binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader) binaryData = parcel.readParcelable(BinaryData::class.java.classLoader)
} }
override fun readParentParcelable(parcel: Parcel): GroupKDB? { override fun readParentParcelable(parcel: Parcel): GroupKDB? {
@@ -150,7 +151,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
fun putAttachment(attachment: Attachment) { fun putAttachment(attachment: Attachment) {
this.binaryDescription = attachment.name this.binaryDescription = attachment.name
this.binaryData = attachment.binaryAttachment this.binaryData = attachment.binaryData
} }
fun removeAttachment(attachment: Attachment? = null) { fun removeAttachment(attachment: Attachment? = null) {

View File

@@ -23,12 +23,9 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.BinaryPool import com.kunzisoft.keepass.database.element.database.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
@@ -48,19 +45,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
@Transient @Transient
private var mDecodeRef = false private var mDecodeRef = false
override var icon: IconImage
get() {
return when {
iconCustom.isUnknown -> super.icon
else -> iconCustom
}
}
set(value) {
if (value is IconImageStandard)
iconCustom = IconImageCustom.UNKNOWN_ICON
super.icon = value
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
var customData = LinkedHashMap<String, String>() var customData = LinkedHashMap<String, String>()
var fields = LinkedHashMap<String, ProtectedString>() var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId> var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
@@ -72,7 +56,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
var additional = "" var additional = ""
var tags = "" var tags = ""
fun getSize(binaryPool: BinaryPool): Long { fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) { for (entry in fields.entries) {
@@ -80,7 +64,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
size += entry.value.length().toLong() size += entry.value.length().toLong()
} }
size += getAttachmentsSize(binaryPool) size += getAttachmentsSize(attachmentPool)
size += autoType.defaultSequence.length.toLong() size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) { for ((key, value) in autoType.entrySet()) {
@@ -89,7 +73,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
} }
for (entry in history) { for (entry in history) {
size += entry.getSize(binaryPool) size += entry.getSize(attachmentPool)
} }
size += overrideURL.length.toLong() size += overrideURL.length.toLong()
@@ -103,7 +87,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor() : super() constructor() : super()
constructor(parcel: Parcel) : super(parcel) { constructor(parcel: Parcel) : super(parcel) {
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
usageCount = UnsignedLong(parcel.readLong()) usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel) customData = ParcelableUtil.readStringParcelableMap(parcel)
@@ -121,7 +104,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags) super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toKotlinLong()) dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags) dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData) ParcelableUtil.writeStringParcelableMap(dest, customData)
@@ -143,7 +125,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
*/ */
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) { fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
super.updateWith(source) super.updateWith(source)
iconCustom = IconImageCustom(source.iconCustom)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map // Add all custom elements in map
@@ -281,16 +262,16 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
/** /**
* It's a list because history labels can be defined multiple times * It's a list because history labels can be defined multiple times
*/ */
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> { fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
val entryAttachmentList = ArrayList<Attachment>() val entryAttachmentList = ArrayList<Attachment>()
for ((label, poolId) in binaries) { for ((label, poolId) in binaries) {
binaryPool[poolId]?.let { binary -> attachmentPool[poolId]?.let { binary ->
entryAttachmentList.add(Attachment(label, binary)) entryAttachmentList.add(Attachment(label, binary))
} }
} }
if (inHistory) { if (inHistory) {
history.forEach { history.forEach {
entryAttachmentList.addAll(it.getAttachments(binaryPool, false)) entryAttachmentList.addAll(it.getAttachments(attachmentPool, false))
} }
} }
return entryAttachmentList return entryAttachmentList
@@ -300,8 +281,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return binaries.isNotEmpty() return binaries.isNotEmpty()
} }
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment) binaries[attachment.name] = attachmentPool.put(attachment.binaryData)
} }
fun removeAttachment(attachment: Attachment) { fun removeAttachment(attachment: Attachment) {
@@ -312,11 +293,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
binaries.clear() binaries.clear()
} }
private fun getAttachmentsSize(binaryPool: BinaryPool): Long { private fun getAttachmentsSize(attachmentPool: AttachmentPool): Long {
var size = 0L var size = 0L
for ((label, poolId) in binaries) { for ((label, poolId) in binaries) {
size += label.length.toLong() size += label.length.toLong()
size += binaryPool[poolId]?.length() ?: 0 size += attachmentPool[poolId]?.getSize() ?: 0
} }
return size return size
} }
@@ -333,7 +314,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
history.add(entry) history.add(entry)
} }
fun removeEntryFromHistory(position: Int): EntryKDBX? { fun removeEntryFromHistory(position: Int): EntryKDBX {
return history.removeAt(position) return history.removeAt(position)
} }

View File

@@ -82,10 +82,6 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
this.nodeId = NodeIdInt(groupId) this.nodeId = NodeIdInt(groupId)
} }
override fun allowAddEntryIfIsRoot(): Boolean {
return false
}
companion object { companion object {
@JvmField @JvmField

View File

@@ -21,37 +21,18 @@ package com.kunzisoft.keepass.database.element.group
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.utils.UnsignedLong import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
import java.util.HashMap
import java.util.UUID
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface { class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
// TODO Encapsulate
override var icon: IconImage
get() {
return if (iconCustom.isUnknown)
super.icon
else
iconCustom
}
set(value) {
if (value is IconImageStandard)
iconCustom = IconImageCustom.UNKNOWN_ICON
super.icon = value
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
private val customData = HashMap<String, String>() private val customData = HashMap<String, String>()
var notes = "" var notes = ""
@@ -77,7 +58,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor() : super() constructor() : super()
constructor(parcel: Parcel) : super(parcel) { constructor(parcel: Parcel) : super(parcel) {
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
usageCount = UnsignedLong(parcel.readLong()) usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel); // TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
@@ -101,7 +81,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags) super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toKotlinLong()) dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags) dest.writeParcelable(locationChanged, flags)
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData); // TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
@@ -115,7 +94,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
fun updateWith(source: GroupKDBX) { fun updateWith(source: GroupKDBX) {
super.updateWith(source) super.updateWith(source)
iconCustom = IconImageCustom(source.iconCustom)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map // Add all custom elements in map
@@ -147,10 +125,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return customData.isNotEmpty() return customData.isNotEmpty()
} }
override fun allowAddEntryIfIsRoot(): Boolean {
return true
}
companion object { companion object {
@JvmField @JvmField

View File

@@ -38,8 +38,6 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
fun removeChildren() fun removeChildren()
fun allowAddEntryIfIsRoot(): Boolean
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>, fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
groupHandler: NodeHandler<Group>) { groupHandler: NodeHandler<Group>) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Jeremy Jamet / Kunzisoft. * Copyright 2021 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -19,19 +19,69 @@
*/ */
package com.kunzisoft.keepass.database.element.icon package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
abstract class IconImage protected constructor() : Parcelable { class IconImage() : IconImageDraw(), Parcelable {
abstract val iconId: Int var standard: IconImageStandard = IconImageStandard()
abstract val isUnknown: Boolean var custom: IconImageCustom = IconImageCustom()
abstract val isMetaStreamIcon: Boolean
constructor(iconImageStandard: IconImageStandard) : this() {
this.standard = iconImageStandard
}
constructor(iconImageCustom: IconImageCustom) : this() {
this.custom = iconImageCustom
}
constructor(iconImageStandard: IconImageStandard,
iconImageCustom: IconImageCustom) : this() {
this.standard = iconImageStandard
this.custom = iconImageCustom
}
constructor(parcel: Parcel) : this() {
standard = parcel.readParcelable(IconImageStandard::class.java.classLoader) ?: standard
custom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: custom
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(standard, flags)
parcel.writeParcelable(custom, flags)
}
override fun describeContents(): Int { override fun describeContents(): Int {
return 0 return 0
} }
companion object { override fun getIconImageToDraw(): IconImage {
const val UNKNOWN_ID = -1 return this
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is IconImage) return false
if (standard != other.standard) return false
if (custom != other.custom) return false
return true
}
override fun hashCode(): Int {
var result = standard.hashCode()
result = 31 * result + custom.hashCode()
return result
}
companion object CREATOR : Parcelable.Creator<IconImage> {
override fun createFromParcel(parcel: Parcel): IconImage {
return IconImage(parcel)
}
override fun newArray(size: Int): Array<IconImage?> {
return arrayOfNulls(size)
}
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2021 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -22,39 +22,30 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.*
import java.util.UUID class IconImageCustom : Parcelable, IconImageDraw {
class IconImageCustom : IconImage { var uuid: UUID
val uuid: UUID constructor() {
@Transient uuid = DatabaseVersioned.UUID_ZERO
var imageData: ByteArray = ByteArray(0)
constructor(uuid: UUID, data: ByteArray) : super() {
this.uuid = uuid
this.imageData = data
} }
constructor(uuid: UUID) : super() { constructor(uuid: UUID) {
this.uuid = uuid this.uuid = uuid
this.imageData = ByteArray(0)
}
constructor(icon: IconImageCustom) : super() {
uuid = icon.uuid
imageData = icon.imageData
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
uuid = parcel.readSerializable() as UUID uuid = parcel.readSerializable() as UUID
// TODO Take too much memories }
// parcel.readByteArray(imageData);
override fun describeContents(): Int {
return 0
} }
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(uuid) dest.writeSerializable(uuid)
// Too big for a parcelable dest.writeByteArray(imageData);
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@@ -64,6 +55,10 @@ class IconImageCustom : IconImage {
return result return result
} }
override fun getIconImageToDraw(): IconImage {
return IconImage(this)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) if (this === other)
return true return true
@@ -74,17 +69,10 @@ class IconImageCustom : IconImage {
return uuid == other.uuid return uuid == other.uuid
} }
override val iconId: Int val isUnknown: Boolean
get() = UNKNOWN_ID get() = uuid == DatabaseVersioned.UUID_ZERO
override val isUnknown: Boolean
get() = this == UNKNOWN_ICON
override val isMetaStreamIcon: Boolean
get() = false
companion object { companion object {
val UNKNOWN_ICON = IconImageCustom(DatabaseVersioned.UUID_ZERO, ByteArray(0))
@JvmField @JvmField
val CREATOR: Parcelable.Creator<IconImageCustom> = object : Parcelable.Creator<IconImageCustom> { val CREATOR: Parcelable.Creator<IconImageCustom> = object : Parcelable.Creator<IconImageCustom> {

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
abstract class IconImageDraw {
var selected = false
/**
* Only to retrieve an icon image to Draw, to not use as object to manipulate
*/
abstract fun getIconImageToDraw(): IconImage
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2019 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
import org.apache.commons.collections.map.AbstractReferenceMap
import org.apache.commons.collections.map.ReferenceMap
import java.util.UUID
class IconImageFactory {
/** customIconMap
* Cache for icon drawable.
* Keys: Integer, Values: IconImageStandard
*/
private val cache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
/** standardIconMap
* Cache for icon drawable.
* Keys: UUID, Values: IconImageCustom
*/
private val customCache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
val unknownIcon: IconImageStandard
get() = getIcon(IconImage.UNKNOWN_ID)
val keyIcon: IconImageStandard
get() = getIcon(IconImageStandard.KEY)
val trashIcon: IconImageStandard
get() = getIcon(IconImageStandard.TRASH)
val folderIcon: IconImageStandard
get() = getIcon(IconImageStandard.FOLDER)
fun getIcon(iconId: Int): IconImageStandard {
var icon: IconImageStandard? = cache[iconId] as IconImageStandard?
if (icon == null) {
icon = IconImageStandard(iconId)
cache[iconId] = icon
}
return icon
}
fun getIcon(iconUuid: UUID): IconImageCustom {
var icon: IconImageCustom? = customCache[iconUuid] as IconImageCustom?
if (icon == null) {
icon = IconImageCustom(iconUuid)
customCache[iconUuid] = icon
}
return icon
}
fun put(icon: IconImageCustom) {
customCache[icon.uuid] = icon
}
}

View File

@@ -21,36 +21,46 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
class IconImageStandard : IconImage { class IconImageStandard : Parcelable, IconImageDraw {
val id: Int
constructor() { constructor() {
this.iconId = KEY this.id = KEY_ID
} }
constructor(iconId: Int) { constructor(iconId: Int) {
this.iconId = iconId if (!isCorrectIconId(iconId))
} this.id = KEY_ID
else
constructor(icon: IconImageStandard) { this.id = iconId
this.iconId = icon.iconId
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
iconId = parcel.readInt() id = parcel.readInt()
}
override fun describeContents(): Int {
return 0
} }
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(iconId) dest.writeInt(id)
} }
override fun hashCode(): Int { override fun hashCode(): Int {
val prime = 31 val prime = 31
var result = 1 var result = 1
result = prime * result + iconId result = prime * result + id
return result return result
} }
override fun getIconImageToDraw(): IconImage {
return IconImage(this)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) if (this === other)
return true return true
@@ -59,22 +69,18 @@ class IconImageStandard : IconImage {
if (other !is IconImageStandard) { if (other !is IconImageStandard) {
return false return false
} }
return iconId == other.iconId return id == other.id
} }
override val iconId: Int
override val isUnknown: Boolean
get() = iconId == UNKNOWN_ID
override val isMetaStreamIcon: Boolean
get() = iconId == 0
companion object { companion object {
const val KEY = 0 const val KEY_ID = 0
const val TRASH = 43 const val TRASH_ID = 43
const val FOLDER = 48 const val FOLDER_ID = 48
fun isCorrectIconId(iconId: Int): Boolean {
return iconId in 0 until NB_ICONS
}
@JvmField @JvmField
val CREATOR: Parcelable.Creator<IconImageStandard> = object : Parcelable.Creator<IconImageStandard> { val CREATOR: Parcelable.Creator<IconImageStandard> = object : Parcelable.Creator<IconImageStandard> {

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
import android.util.Log
import com.kunzisoft.keepass.database.element.database.BinaryByte
import com.kunzisoft.keepass.database.element.database.BinaryByte.Companion.MAX_BINARY_BYTES
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.database.BinaryFile
import com.kunzisoft.keepass.database.element.database.CustomIconPool
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
import java.io.File
import java.util.*
class IconsManager {
private val standardCache = List(NB_ICONS) {
IconImageStandard(it)
}
private val customCache = CustomIconPool()
fun getIcon(iconId: Int): IconImageStandard {
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
return standardCache[searchIconId]
}
fun doForEachStandardIcon(action: (IconImageStandard) -> Unit) {
standardCache.forEach { icon ->
action.invoke(icon)
}
}
/*
* Custom
*/
fun buildNewCustomIcon(cacheDirectory: File,
key: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) {
// Create a binary file for a brand new custom icon
addCustomIcon(cacheDirectory, key, -1, result)
}
fun addCustomIcon(cacheDirectory: File,
key: UUID? = null,
dataSize: Int,
result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = customCache.put(key) { uniqueBinaryId ->
// Create a byte array for better performance with small data
if (dataSize in 1..MAX_BINARY_BYTES) {
BinaryByte()
} else {
val fileInCache = File(cacheDirectory, uniqueBinaryId)
BinaryFile(fileInCache)
}
}
result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary)
}
fun getIcon(iconUuid: UUID): IconImageCustom {
return IconImageCustom(iconUuid)
}
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return customCache.isBinaryDuplicate(binaryData)
}
fun removeCustomIcon(iconUuid: UUID) {
val binary = customCache[iconUuid]
customCache.remove(iconUuid)
try {
binary?.clear()
} catch (e: Exception) {
Log.w(TAG, "Unable to remove custom icon binary", e)
}
}
fun getBinaryForCustomIcon(iconUuid: UUID): BinaryData? {
return customCache[iconUuid]
}
fun doForEachCustomIcon(action: (IconImageCustom, BinaryData) -> Unit) {
customCache.doForEachBinary { key, binary ->
action.invoke(IconImageCustom(key), binary)
}
}
/**
* Clear the cache of icons
*/
fun clearCache() {
try {
customCache.clear()
} catch(e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}
}
companion object {
private val TAG = IconsManager::class.java.name
}
}

View File

@@ -22,11 +22,10 @@ package com.kunzisoft.keepass.database.element.node
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import org.joda.time.LocalDateTime import org.joda.time.LocalDateTime
/** /**
@@ -88,7 +87,7 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
final override var parent: Parent? = null final override var parent: Parent? = null
override var icon: IconImage = IconImageStandard() final override var icon: IconImage = IconImage()
final override var creationTime: DateInstant = DateInstant() final override var creationTime: DateInstant = DateInstant()

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.database.file.input package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -37,10 +38,12 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
* *
* @throws LoadDatabaseException on database error (contains IO exceptions) * @throws LoadDatabaseException on database error (contains IO exceptions)
*/ */
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb fixDuplicateUUID: Boolean = false): PwDb
@@ -48,6 +51,7 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray, masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb fixDuplicateUUID: Boolean = false): PwDb
} }

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -48,26 +49,30 @@ import javax.crypto.spec.SecretKeySpec
class DatabaseInputKDB(cacheDirectory: File) class DatabaseInputKDB(cacheDirectory: File)
: DatabaseInput<DatabaseKDB>(cacheDirectory) { : DatabaseInput<DatabaseKDB>(cacheDirectory) {
private lateinit var mDatabaseToOpen: DatabaseKDB private lateinit var mDatabase: DatabaseKDB
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB { fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream)
} }
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray, masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB { fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.masterKey = masterKey mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
} }
} }
@@ -101,38 +106,38 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabaseToOpen = DatabaseKDB() mDatabase = DatabaseKDB()
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke() assignMasterKey?.invoke()
// Select algorithm // Select algorithm
when { when {
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> { header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael mDatabase.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
} }
header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> { header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish mDatabase.encryptionAlgorithm = EncryptionAlgorithm.Twofish
} }
else -> throw InvalidAlgorithmDatabaseException() else -> throw InvalidAlgorithmDatabaseException()
} }
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong() mDatabase.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
// Generate transformedMasterKey from masterKey // Generate transformedMasterKey from masterKey
mDatabaseToOpen.makeFinalKey( mDatabase.makeFinalKey(
header.masterSeed, header.masterSeed,
header.transformSeed, header.transformSeed,
mDatabaseToOpen.numberKeyEncryptionRounds) mDatabase.numberKeyEncryptionRounds)
progressTaskUpdater?.updateMessage(R.string.decrypting_db) progressTaskUpdater?.updateMessage(R.string.decrypting_db)
// Initialize Rijndael algorithm // Initialize Rijndael algorithm
val cipher: Cipher = try { val cipher: Cipher = try {
when { when {
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> { mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
CipherFactory.getInstance("AES/CBC/PKCS5Padding") CipherFactory.getInstance("AES/CBC/PKCS5Padding")
} }
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> { mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING") CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
} }
else -> throw IOException("Encryption algorithm is not supported") else -> throw IOException("Encryption algorithm is not supported")
@@ -145,7 +150,7 @@ class DatabaseInputKDB(cacheDirectory: File)
try { try {
cipher.init(Cipher.DECRYPT_MODE, cipher.init(Cipher.DECRYPT_MODE,
SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), SecretKeySpec(mDatabase.finalKey, "AES"),
IvParameterSpec(header.encryptionIV)) IvParameterSpec(header.encryptionIV))
} catch (e1: InvalidKeyException) { } catch (e1: InvalidKeyException) {
throw IOException("Invalid key") throw IOException("Invalid key")
@@ -169,9 +174,10 @@ class DatabaseInputKDB(cacheDirectory: File)
) )
// New manual root because KDB contains multiple root groups (here available with getRootGroups()) // New manual root because KDB contains multiple root groups (here available with getRootGroups())
val newRoot = mDatabaseToOpen.createGroup() val newRoot = mDatabase.createGroup()
newRoot.level = -1 newRoot.level = -1
mDatabaseToOpen.rootGroup = newRoot mDatabase.rootGroup = newRoot
mDatabase.addGroupIndex(newRoot)
// Import all nodes // Import all nodes
var newGroup: GroupKDB? = null var newGroup: GroupKDB? = null
@@ -192,12 +198,12 @@ class DatabaseInputKDB(cacheDirectory: File)
// Create new node depending on byte number // Create new node depending on byte number
when (fieldSize) { when (fieldSize) {
4 -> { 4 -> {
newGroup = mDatabaseToOpen.createGroup().apply { newGroup = mDatabase.createGroup().apply {
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt()) setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} }
} }
16 -> { 16 -> {
newEntry = mDatabaseToOpen.createEntry().apply { newEntry = mDatabase.createEntry().apply {
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid()) nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
} }
} }
@@ -211,7 +217,7 @@ class DatabaseInputKDB(cacheDirectory: File)
group.title = cipherInputStream.readBytesToString(fieldSize) group.title = cipherInputStream.readBytesToString(fieldSize)
} ?: } ?:
newEntry?.let { entry -> newEntry?.let { entry ->
val groupKDB = mDatabaseToOpen.createGroup() val groupKDB = mDatabase.createGroup()
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt()) groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
entry.parent = groupKDB entry.parent = groupKDB
} }
@@ -226,7 +232,7 @@ class DatabaseInputKDB(cacheDirectory: File)
if (iconId == -1) { if (iconId == -1) {
iconId = 0 iconId = 0
} }
entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId) entry.icon.standard = mDatabase.getStandardIcon(iconId)
} }
} }
0x0004 -> { 0x0004 -> {
@@ -255,7 +261,7 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
0x0007 -> { 0x0007 -> {
newGroup?.let { group -> newGroup?.let { group ->
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) group.icon.standard = mDatabase.getStandardIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} ?: } ?:
newEntry?.let { entry -> newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false) entry.password = cipherInputStream.readBytesToString(fieldSize,false)
@@ -300,11 +306,12 @@ class DatabaseInputKDB(cacheDirectory: File)
0x000E -> { 0x000E -> {
newEntry?.let { entry -> newEntry?.let { entry ->
if (fieldSize > 0) { if (fieldSize > 0) {
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory) val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory)
entry.binaryData = binaryAttachment entry.binaryData = binaryAttachment
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream -> val cipherKey = mDatabase.loadedCipherKey
cipherInputStream.readBytes(fieldSize, ?: throw IOException("Unable to retrieve cipher key to load binaries")
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer -> BufferedOutputStream(binaryAttachment.getOutputDataStream(cipherKey)).use { outputStream ->
cipherInputStream.readBytes(fieldSize) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
} }
} }
@@ -314,12 +321,12 @@ class DatabaseInputKDB(cacheDirectory: File)
0xFFFF -> { 0xFFFF -> {
// End record. Save node and count it. // End record. Save node and count it.
newGroup?.let { group -> newGroup?.let { group ->
mDatabaseToOpen.addGroupIndex(group) mDatabase.addGroupIndex(group)
currentGroupNumber++ currentGroupNumber++
newGroup = null newGroup = null
} }
newEntry?.let { entry -> newEntry?.let { entry ->
mDatabaseToOpen.addEntryIndex(entry) mDatabase.addEntryIndex(entry)
currentEntryNumber++ currentEntryNumber++
newEntry = null newEntry = null
} }
@@ -337,20 +344,20 @@ class DatabaseInputKDB(cacheDirectory: File)
constructTreeFromIndex() constructTreeFromIndex()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw e throw e
} catch (e: IOException) { } catch (e: IOException) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw IODatabaseException(e) throw IODatabaseException(e)
} catch (e: OutOfMemoryError) { } catch (e: OutOfMemoryError) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw NoMemoryDatabaseException(e) throw NoMemoryDatabaseException(e)
} catch (e: Exception) { } catch (e: Exception) {
mDatabaseToOpen.clearCache() mDatabase.clearCache()
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} }
return mDatabaseToOpen return mDatabase
} }
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) { private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) {
@@ -375,18 +382,18 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
private fun constructTreeFromIndex() { private fun constructTreeFromIndex() {
mDatabaseToOpen.rootGroup?.let { mDatabase.rootGroup?.let {
// add each group // add each group
val groupIterator = mDatabaseToOpen.getGroupIndexes().iterator() val groupIterator = mDatabase.getGroupIndexes().iterator()
if (groupIterator.hasNext()) if (groupIterator.hasNext())
buildTreeGroups(it, groupIterator.next(), groupIterator) buildTreeGroups(it, groupIterator.next(), groupIterator)
// add each child // add each child
for (currentEntry in mDatabaseToOpen.getEntryIndexes()) { for (currentEntry in mDatabase.getEntryIndexes()) {
if (currentEntry.parent != null) { if (currentEntry.parent != null) {
// Only the parent id is known so complete the info // Only the parent id is known so complete the info
val parentGroupRetrieve = mDatabaseToOpen.getGroupById(currentEntry.parent!!.nodeId) val parentGroupRetrieve = mDatabase.getGroupById(currentEntry.parent!!.nodeId)
parentGroupRetrieve?.addChildEntry(currentEntry) parentGroupRetrieve?.addChildEntry(currentEntry)
currentEntry.parent = parentGroupRetrieve currentEntry.parent = parentGroupRetrieve
} }

View File

@@ -26,16 +26,16 @@ import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -78,7 +78,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
private var ctxStringName: String? = null private var ctxStringName: String? = null
private var ctxStringValue: ProtectedString? = null private var ctxStringValue: ProtectedString? = null
private var ctxBinaryName: String? = null private var ctxBinaryName: String? = null
private var ctxBinaryValue: BinaryAttachment? = null private var ctxBinaryValue: BinaryData? = null
private var ctxATName: String? = null private var ctxATName: String? = null
private var ctxATSeq: String? = null private var ctxATSeq: String? = null
private var entryInHistory = false private var entryInHistory = false
@@ -96,20 +96,24 @@ class DatabaseInputKDBX(cacheDirectory: File)
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX { fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.retrieveMasterKey(password, keyInputStream) mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream)
} }
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray, masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX { fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey mDatabase.masterKey = masterKey
} }
} }
@@ -272,9 +276,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
val byteLength = size - 1 val byteLength = size - 1
// No compression at this level // No compression at this level
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag) val protectedBinary = mDatabase.buildNewAttachment(cacheDirectory, false, protectedFlag)
protectedBinary.getOutputDataStream().use { outputStream -> val cipherKey = mDatabase.loadedCipherKey
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer -> ?: throw IOException("Unable to retrieve cipher key to load binaries")
protectedBinary.getOutputDataStream(cipherKey).use { outputStream ->
dataInputStream.readBytes(byteLength) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
} }
} }
@@ -500,9 +506,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
ctxGroup?.notes = readString(xpp) ctxGroup?.notes = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp)) ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
return switchContext(ctx, KdbContext.GroupTimes, xpp) return switchContext(ctx, KdbContext.GroupTimes, xpp)
} else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) {
@@ -554,9 +560,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) { KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp)) ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp)) ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
ctxEntry?.foregroundColor = readString(xpp) ctxEntry?.foregroundColor = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
@@ -698,9 +704,13 @@ class DatabaseInputKDBX(cacheDirectory: File)
return KdbContext.Meta return KdbContext.Meta
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) { } else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) { if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) {
val icon = IconImageCustom(customIconID, customIconData!!) mDatabase.addCustomIcon(cacheDirectory, customIconID, customIconData!!.size) { _, binary ->
mDatabase.addCustomIcon(icon) mDatabase.loadedCipherKey?.let { cipherKey ->
mDatabase.iconFactory.put(icon) binary?.getOutputDataStream(cipherKey)?.use { outputStream ->
outputStream.write(customIconData)
}
}
}
} }
customIconID = DatabaseVersioned.UUID_ZERO customIconID = DatabaseVersioned.UUID_ZERO
@@ -956,7 +966,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
} }
@Throws(XmlPullParserException::class, IOException::class) @Throws(XmlPullParserException::class, IOException::class)
private fun readBinary(xpp: XmlPullParser): BinaryAttachment? { private fun readBinary(xpp: XmlPullParser): BinaryData? {
// Reference Id to a binary already present in binary pool // Reference Id to a binary already present in binary pool
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef) val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
@@ -971,7 +981,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
var binaryRetrieve = mDatabase.binaryPool[id] var binaryRetrieve = mDatabase.binaryPool[id]
// Create empty binary if not retrieved in pool // Create empty binary if not retrieved in pool
if (binaryRetrieve == null) { if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory, binaryRetrieve = mDatabase.buildNewAttachment(cacheDirectory,
compression = false, protection = false, binaryPoolId = id) compression = false, protection = false, binaryPoolId = id)
} }
return binaryRetrieve return binaryRetrieve
@@ -987,7 +997,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
} }
@Throws(IOException::class, XmlPullParserException::class) @Throws(IOException::class, XmlPullParserException::class)
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? { private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryData? {
var compressed = false var compressed = false
var protected = true var protected = true
@@ -1008,15 +1018,17 @@ class DatabaseInputKDBX(cacheDirectory: File)
return null return null
// Build the new binary and compress // Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId) val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory, compressed, protected, binaryId)
val binaryCipherKey = mDatabase.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to load binaries")
try { try {
binaryAttachment.getOutputDataStream().use { outputStream -> binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
outputStream.write(Base64.decode(base64, BASE_64_FLAG)) outputStream.write(Base64.decode(base64, BASE_64_FLAG))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to read base 64 attachment", e) Log.e(TAG, "Unable to read base 64 attachment", e)
binaryAttachment.isCorrupted = true binaryAttachment.isCorrupted = true
binaryAttachment.getOutputDataStream().use { outputStream -> binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
outputStream.write(base64.toByteArray()) outputStream.write(base64.toByteArray())
} }
} }
@@ -1083,6 +1095,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
return xpp return xpp
} }
} }
} }
@Throws(IOException::class, XmlPullParserException::class) @Throws(IOException::class, XmlPullParserException::class)

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
@@ -197,15 +197,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
@Suppress("CAST_NEVER_SUCCEEDS") @Suppress("CAST_NEVER_SUCCEEDS")
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
fun outputPlanGroupAndEntries(os: OutputStream) { fun outputPlanGroupAndEntries(outputStream: OutputStream) {
val los = LittleEndianDataOutputStream(os) val littleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
// useHeaderHash // useHeaderHash
if (headerHashBlock != null) { if (headerHashBlock != null) {
try { try {
los.writeUShort(0x0000) littleEndianDataOutputStream.writeUShort(0x0000)
los.writeInt(headerHashBlock!!.size) littleEndianDataOutputStream.writeInt(headerHashBlock!!.size)
los.write(headerHashBlock!!) littleEndianDataOutputStream.write(headerHashBlock!!)
} catch (e: IOException) { } catch (e: IOException) {
throw DatabaseOutputException("Failed to output header hash.", e) throw DatabaseOutputException("Failed to output header hash.", e)
} }
@@ -213,20 +213,11 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// Groups // Groups
mDatabaseKDB.doForEachGroupInIndex { group -> mDatabaseKDB.doForEachGroupInIndex { group ->
val pgo = GroupOutputKDB(group, os) GroupOutputKDB(group, outputStream).output()
try {
pgo.output()
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output a tree", e)
}
} }
// Entries
mDatabaseKDB.doForEachEntryInIndex { entry -> mDatabaseKDB.doForEachEntryInIndex { entry ->
val peo = EntryOutputKDB(entry, os) EntryOutputKDB(entry, outputStream, mDatabaseKDB.loadedCipherKey).output()
try {
peo.output()
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output an entry.", e)
}
} }
} }

View File

@@ -32,11 +32,9 @@ import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -138,27 +136,28 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
dataOutputStream.writeInt(streamKeySize) dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey) dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachOrderedBinary { _, keyBinary -> database.loadedCipherKey?.let { binaryCipherKey ->
val protectedBinary = keyBinary.binary database.binaryPool.doForEachOrderedBinaryWithoutDuplication { _, binary ->
// Force decompression to add binary in header // Force decompression to add binary in header
protectedBinary.decompress() binary.decompress(binaryCipherKey)
// Write type binary // Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size // Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1)) dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1))
// Write protected flag // Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) { if (binary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
} }
dataOutputStream.writeByte(flag) dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream -> binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.readAllBytes { buffer ->
dataOutputStream.write(buffer) dataOutputStream.write(buffer)
}
} }
} }
} } ?: Log.e(TAG, "Unable to retrieve cipher key to write head binaries")
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader) dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
dataOutputStream.writeInt(0) dataOutputStream.writeInt(0)
@@ -362,10 +361,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeUuid(DatabaseKDBXXML.ElemUuid, group.id) writeUuid(DatabaseKDBXXML.ElemUuid, group.id)
writeObject(DatabaseKDBXXML.ElemName, group.title) writeObject(DatabaseKDBXXML.ElemName, group.title)
writeObject(DatabaseKDBXXML.ElemNotes, group.notes) writeObject(DatabaseKDBXXML.ElemNotes, group.notes)
writeObject(DatabaseKDBXXML.ElemIcon, group.icon.iconId.toLong()) writeObject(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong())
if (group.iconCustom != IconImageCustom.UNKNOWN_ICON) { if (!group.icon.custom.isUnknown) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.iconCustom.uuid) writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid)
} }
writeTimes(group) writeTimes(group)
@@ -387,10 +386,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemEntry) xml.startTag(null, DatabaseKDBXXML.ElemEntry)
writeUuid(DatabaseKDBXXML.ElemUuid, entry.id) writeUuid(DatabaseKDBXXML.ElemUuid, entry.id)
writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.iconId.toLong()) writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong())
if (entry.iconCustom != IconImageCustom.UNKNOWN_ICON) { if (!entry.icon.custom.isUnknown) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.iconCustom.uuid) writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid)
} }
writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor) writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
@@ -473,7 +472,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (binary.isProtected) { if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
binary.getInputDataStream().use { inputStream -> binary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.readBytes { buffer ->
val encoded = ByteArray(buffer.size) val encoded = ByteArray(buffer.size)
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0) randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
@@ -482,7 +481,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} else { } else {
// Write the XML // Write the XML
binary.getInputDataStream().use { inputStream -> binary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.readBytes { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
} }
} }
@@ -495,28 +494,31 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
// With kdbx4, don't use this method because binaries are in header file // With kdbx4, don't use this method because binaries are in header file
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeMetaBinaries() { private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries) mDatabaseKDBX.loadedCipherKey?.let { binaryCipherKey ->
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
// Use indexes because necessarily (binary header ref is the order) // Use indexes because necessarily (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary -> mDatabaseKDBX.binaryPool.doForEachOrderedBinaryWithoutDuplication { index, binary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary) xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
val binary = keyBinary.binary if (binary.getSize() > 0) {
if (binary.length() > 0) { if (binary.isCompressed) {
if (binary.isCompressed) { xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) }
} try {
// Write the XML // Write the XML
binary.getInputDataStream().use { inputStream -> binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.readAllBytes { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to write binary", e)
} }
} }
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
} }
xml.endTag(null, DatabaseKDBXXML.ElemBinary) xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
} } ?: Log.e(TAG, "Unable to retrieve cipher key to write binaries")
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
@@ -589,7 +591,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
} }
} else { } else {
xml.text(safeXmlString(value.toString())) xml.text(value.toString())
} }
xml.endTag(null, DatabaseKDBXXML.ElemValue) xml.endTag(null, DatabaseKDBXXML.ElemValue)
@@ -697,38 +699,58 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeCustomIconList() { private fun writeCustomIconList() {
val customIcons = mDatabaseKDBX.customIcons mDatabaseKDBX.loadedCipherKey?.let { cipherKey ->
if (customIcons.size == 0) return var firstElement = true
mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary ->
if (binary.dataExists()) {
// Write the parent tag
if (firstElement) {
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
firstElement = false
}
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons) xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
for (icon in customIcons) { writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid)
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem) var customImageData = ByteArray(0)
try {
binary.getInputDataStream(cipherKey).use { inputStream ->
customImageData = inputStream.readBytes()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to write custom icon", e)
} finally {
writeObject(DatabaseKDBXXML.ElemCustomIconItemData,
String(Base64.encode(customImageData, BASE_64_FLAG)))
}
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, icon.uuid) xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
writeObject(DatabaseKDBXXML.ElemCustomIconItemData, String(Base64.encode(icon.imageData, BASE_64_FLAG))) }
}
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem) // Close the parent tag
} if (!firstElement) {
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons) }
} ?: Log.e(TAG, "Unable to retrieve cipher key to write custom icons")
} }
private fun safeXmlString(text: String): String { private fun safeXmlString(text: String): String {
if (text.isEmpty()) { if (text.isEmpty()) {
return text return text
} }
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
var ch: Char var character: Char
for (element in text) { for (element in text) {
ch = element character = element
val hexChar = character.toInt()
if ( if (
ch.toInt() in 0x20..0xD7FF || hexChar in 0x20..0xD7FF ||
ch.toInt() == 0x9 || ch.toInt() == 0xA || ch.toInt() == 0xD || hexChar == 0x9 ||
ch.toInt() in 0xE000..0xFFFD hexChar == 0xA ||
hexChar == 0xD ||
hexChar in 0xE000..0xFFFD
) { ) {
stringBuilder.append(ch) stringBuilder.append(character)
} }
} }
return stringBuilder.toString() return stringBuilder.toString()

View File

@@ -19,9 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import android.util.Log
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.file.output.GroupOutputKDB.Companion.GROUPID_FIELD_SIZE import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
@@ -29,96 +30,93 @@ import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
class EntryOutputKDB
/** /**
* Output the GroupKDB to the stream * Output the GroupKDB to the stream
*/ */
(private val mEntry: EntryKDB, private val mOutputStream: OutputStream) { class EntryOutputKDB(private val mEntry: EntryKDB,
/** private val mOutputStream: OutputStream,
* Returns the number of bytes written by the stream private val mCipherKey: Database.LoadedKey?) {
* @return Number of bytes written
*/
var length: Long = 0
private set
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
@Throws(IOException::class) @Throws(DatabaseOutputException::class)
fun output() { fun output() {
try {
// UUID
mOutputStream.write(UUID_FIELD_TYPE)
mOutputStream.write(UUID_FIELD_SIZE)
mOutputStream.write(uuidTo16Bytes(mEntry.id))
length += 134 // Length of fixed size fields // Group ID
mOutputStream.write(GROUPID_FIELD_TYPE)
mOutputStream.write(GROUPID_FIELD_SIZE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id)))
// UUID // Image ID
mOutputStream.write(UUID_FIELD_TYPE) mOutputStream.write(IMAGEID_FIELD_TYPE)
mOutputStream.write(UUID_FIELD_SIZE) mOutputStream.write(IMAGEID_FIELD_SIZE)
mOutputStream.write(uuidTo16Bytes(mEntry.id)) mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.standard.id)))
// Group ID // Title
mOutputStream.write(GROUPID_FIELD_TYPE) //byte[] title = mEntry.title.getBytes("UTF-8");
mOutputStream.write(GROUPID_FIELD_SIZE) mOutputStream.write(TITLE_FIELD_TYPE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id))) StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.title)
// Image ID // URL
mOutputStream.write(IMAGEID_FIELD_TYPE) mOutputStream.write(URL_FIELD_TYPE)
mOutputStream.write(IMAGEID_FIELD_SIZE) StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.url)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId)))
// Title // Username
//byte[] title = mEntry.title.getBytes("UTF-8"); mOutputStream.write(USERNAME_FIELD_TYPE)
mOutputStream.write(TITLE_FIELD_TYPE) StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.username)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong()
// URL // Password
mOutputStream.write(URL_FIELD_TYPE) mOutputStream.write(PASSWORD_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong() writePassword(mEntry.password, mOutputStream)
// Username // Additional
mOutputStream.write(USERNAME_FIELD_TYPE) mOutputStream.write(ADDITIONAL_FIELD_TYPE)
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong() StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.notes)
// Password // Create date
mOutputStream.write(PASSWORD_FIELD_TYPE) writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
length += writePassword(mEntry.password, mOutputStream).toLong()
// Additional // Modification date
mOutputStream.write(ADDITIONAL_FIELD_TYPE) writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong()
// Create date // Access date
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date)) writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
// Modification date // Expiration date
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date)) writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
// Access date // Binary description
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date)) mOutputStream.write(BINARY_DESC_FIELD_TYPE)
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription)
// Expiration date // Binary
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date)) mCipherKey?.let { cipherKey ->
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
// Binary description val binaryData = mEntry.binaryData
mOutputStream.write(BINARY_DESC_FIELD_TYPE) val binaryDataLength = binaryData?.getSize() ?: 0L
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong() // Write data length
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
// Binary // Write data
mOutputStream.write(BINARY_DATA_FIELD_TYPE) if (binaryDataLength > 0) {
val binaryData = mEntry.binaryData binaryData?.getInputDataStream(cipherKey).use { inputStream ->
val binaryDataLength = binaryData?.length() ?: 0L inputStream?.readAllBytes { buffer ->
// Write data length mOutputStream.write(buffer)
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength))) }
// Write data inputStream?.close()
if (binaryDataLength > 0) { }
binaryData?.getInputDataStream().use { inputStream ->
inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
length += buffer.size
mOutputStream.write(buffer)
} }
inputStream?.close() } ?: Log.e(TAG, "Unable to retrieve cipher key to write entry binary")
}
}
// End // End
mOutputStream.write(END_FIELD_TYPE) mOutputStream.write(END_FIELD_TYPE)
mOutputStream.write(ZERO_FIELD_SIZE) mOutputStream.write(ZERO_FIELD_SIZE)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output an entry.", e)
}
} }
@Throws(IOException::class) @Throws(IOException::class)
@@ -143,30 +141,30 @@ class EntryOutputKDB
} }
companion object { companion object {
// Constants
val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val TAG = EntryOutputKDB::class.java.name
val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16)) // Constants
val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) private val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00) private val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
private val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
private val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
private val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
private val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
private val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
private val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
} }
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.dateTo5Bytes import com.kunzisoft.keepass.stream.dateTo5Bytes
import com.kunzisoft.keepass.stream.uIntTo4Bytes import com.kunzisoft.keepass.stream.uIntTo4Bytes
import com.kunzisoft.keepass.stream.uShortTo2Bytes import com.kunzisoft.keepass.stream.uShortTo2Bytes
@@ -31,79 +32,84 @@ import java.io.OutputStream
/** /**
* Output the GroupKDB to the stream * Output the GroupKDB to the stream
*/ */
class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: OutputStream) { class GroupOutputKDB(private val mGroup: GroupKDB,
private val mOutputStream: OutputStream) {
@Throws(IOException::class) @Throws(DatabaseOutputException::class)
fun output() { fun output() {
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter. try {
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter.
// Group ID // Group ID
mOutputStream.write(GROUPID_FIELD_TYPE) mOutputStream.write(GROUPID_FIELD_TYPE)
mOutputStream.write(GROUPID_FIELD_SIZE) mOutputStream.write(GROUPID_FIELD_SIZE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id))) mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id)))
// Name // Name
mOutputStream.write(NAME_FIELD_TYPE) mOutputStream.write(NAME_FIELD_TYPE)
StringDatabaseKDBUtils.writeStringToBytes(mGroup.title, mOutputStream) StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mGroup.title)
// Create date // Create date
mOutputStream.write(CREATE_FIELD_TYPE) mOutputStream.write(CREATE_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE) mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date)) mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
// Modification date // Modification date
mOutputStream.write(MOD_FIELD_TYPE) mOutputStream.write(MOD_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE) mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date)) mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
// Access date // Access date
mOutputStream.write(ACCESS_FIELD_TYPE) mOutputStream.write(ACCESS_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE) mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date)) mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
// Expiration date // Expiration date
mOutputStream.write(EXPIRE_FIELD_TYPE) mOutputStream.write(EXPIRE_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE) mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date)) mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
// Image ID // Image ID
mOutputStream.write(IMAGEID_FIELD_TYPE) mOutputStream.write(IMAGEID_FIELD_TYPE)
mOutputStream.write(IMAGEID_FIELD_SIZE) mOutputStream.write(IMAGEID_FIELD_SIZE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId))) mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.standard.id)))
// Level // Level
mOutputStream.write(LEVEL_FIELD_TYPE) mOutputStream.write(LEVEL_FIELD_TYPE)
mOutputStream.write(LEVEL_FIELD_SIZE) mOutputStream.write(LEVEL_FIELD_SIZE)
mOutputStream.write(uShortTo2Bytes(mGroup.level)) mOutputStream.write(uShortTo2Bytes(mGroup.level))
// Flags // Flags
mOutputStream.write(FLAGS_FIELD_TYPE) mOutputStream.write(FLAGS_FIELD_TYPE)
mOutputStream.write(FLAGS_FIELD_SIZE) mOutputStream.write(FLAGS_FIELD_SIZE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags))) mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags)))
// End // End
mOutputStream.write(END_FIELD_TYPE) mOutputStream.write(END_FIELD_TYPE)
mOutputStream.write(ZERO_FIELD_SIZE) mOutputStream.write(ZERO_FIELD_SIZE)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output a group.", e)
}
} }
companion object { companion object {
// Constants // Constants
val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1) private val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) private val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) private val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) private val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2)) private val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2))
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
} }
} }

View File

@@ -81,6 +81,7 @@ class SearchHelper {
max: Int): Group? { max: Int): Group? {
val searchGroup = database.createGroup() val searchGroup = database.createGroup()
searchGroup?.isVirtual = true
searchGroup?.title = "\"" + searchQuery + "\"" searchGroup?.title = "\"" + searchQuery + "\""
// Search all entries // Search all entries

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
@@ -74,6 +75,24 @@ open class Education(val activity: Activity) {
editor.apply() editor.apply()
} }
protected fun getCircleColor(): Int {
val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationCircleColor))
val colorControl = typedArray.getColor(0, Color.GREEN)
typedArray.recycle()
return colorControl
}
protected fun getCircleAlpha(): Float {
return 0.98F
}
protected fun getTextColor(): Int {
val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationTextColor))
val colorControl = typedArray.getColor(0, Color.WHITE)
typedArray.recycle()
return colorControl
}
companion object { companion object {
private const val EDUCATION_PREFERENCE = "kdbxeducation" private const val EDUCATION_PREFERENCE = "kdbxeducation"

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.graphics.Color
import android.view.View import android.view.View
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
@@ -38,7 +37,9 @@ class EntryActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_field_copy_title), activity.getString(R.string.education_field_copy_title),
activity.getString(R.string.education_field_copy_summary)) activity.getString(R.string.education_field_copy_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(false) .tintTarget(false)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -68,7 +69,9 @@ class EntryActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_entry_edit_title), activity.getString(R.string.education_entry_edit_title),
activity.getString(R.string.education_entry_edit_summary)) activity.getString(R.string.education_entry_edit_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.graphics.Color
import android.view.View import android.view.View
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
@@ -40,7 +39,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_generate_password_title), activity.getString(R.string.education_generate_password_title),
activity.getString(R.string.education_generate_password_summary)) activity.getString(R.string.education_generate_password_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -69,7 +70,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_entry_new_field_title), activity.getString(R.string.education_entry_new_field_title),
activity.getString(R.string.education_entry_new_field_summary)) activity.getString(R.string.education_entry_new_field_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -98,7 +101,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_add_attachment_title), activity.getString(R.string.education_add_attachment_title),
activity.getString(R.string.education_add_attachment_summary)) activity.getString(R.string.education_add_attachment_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -127,7 +132,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_setup_OTP_title), activity.getString(R.string.education_setup_OTP_title),
activity.getString(R.string.education_setup_OTP_summary)) activity.getString(R.string.education_setup_OTP_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import androidx.core.content.ContextCompat
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -43,8 +43,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_create_database_title), activity.getString(R.string.education_create_database_title),
activity.getString(R.string.education_create_database_summary)) activity.getString(R.string.education_create_database_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp)) .icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp))
.textColorInt(Color.WHITE)
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -73,8 +75,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_select_database_title), activity.getString(R.string.education_select_database_title),
activity.getString(R.string.education_select_database_summary)) activity.getString(R.string.education_select_database_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp)) .icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp))
.textColorInt(Color.WHITE)
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.graphics.Color
import android.view.View import android.view.View
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
@@ -36,7 +35,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_new_node_title), activity.getString(R.string.education_new_node_title),
activity.getString(R.string.education_new_node_summary)) activity.getString(R.string.education_new_node_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(false) .tintTarget(false)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -61,7 +62,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_search_title), activity.getString(R.string.education_search_title),
activity.getString(R.string.education_search_summary)) activity.getString(R.string.education_search_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -86,7 +89,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_sort_title), activity.getString(R.string.education_sort_title),
activity.getString(R.string.education_sort_summary)) activity.getString(R.string.education_sort_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -111,7 +116,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_lock_title), activity.getString(R.string.education_lock_title),
activity.getString(R.string.education_lock_summary)) activity.getString(R.string.education_lock_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education package com.kunzisoft.keepass.education
import android.app.Activity import android.app.Activity
import android.graphics.Color
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import android.view.View import android.view.View
import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTarget
@@ -37,8 +36,10 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_unlock_title), activity.getString(R.string.education_unlock_title),
activity.getString(R.string.education_unlock_summary)) 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.mipmap.ic_launcher_round))
.textColorInt(Color.WHITE) .textColorInt(getTextColor())
.tintTarget(false) .tintTarget(false)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -63,7 +64,9 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_read_only_title), activity.getString(R.string.education_read_only_title),
activity.getString(R.string.education_read_only_summary)) activity.getString(R.string.education_read_only_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true) .tintTarget(true)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {
@@ -88,7 +91,9 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_advanced_unlock_title), activity.getString(R.string.education_advanced_unlock_title),
activity.getString(R.string.education_advanced_unlock_summary)) activity.getString(R.string.education_advanced_unlock_summary))
.textColorInt(Color.WHITE) .outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(false) .tintTarget(false)
.cancelable(true), .cancelable(true),
object : TapTargetView.Listener() { object : TapTargetView.Listener() {

View File

@@ -26,111 +26,237 @@ import android.graphics.*
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Log import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import org.apache.commons.collections.map.AbstractReferenceMap import kotlinx.coroutines.CoroutineScope
import org.apache.commons.collections.map.ReferenceMap import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.HashMap
/** /**
* Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint * Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint
*/ */
class IconDrawableFactory { class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedKey?,
private val retrieveCustomIconBinary : (iconId: UUID) -> BinaryData?) {
/** customIconMap /** customIconMap
* Cache for icon drawable. * Cache for icon drawable.
* Keys: UUID, Values: Drawables * Keys: UUID, Values: Drawables
*/ */
private val customIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK) private val customIconMap = HashMap<UUID, WeakReference<Drawable>>()
/** standardIconMap /** standardIconMap
* Cache for icon drawable. * Cache for icon drawable.
* Keys: Integer, Values: Drawables * Keys: Integer, Values: Drawables
*/ */
private val standardIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK) private val standardIconMap = HashMap<CacheKey, WeakReference<Drawable>>()
/** /**
* Utility method to assign a drawable to an ImageView and tint it * Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed
*/ */
fun assignDrawableToImageView(superDrawable: SuperDrawable, imageView: ImageView?, tint: Boolean, tintColor: Int) { private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable {
if (imageView != null) { val icon = iconDraw.getIconImageToDraw()
imageView.setImageDrawable(superDrawable.drawable) val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
if (superDrawable.tintable && tint) { if (customIconBinary != null && customIconBinary.dataExists()) {
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor)) getIconDrawable(context.resources, icon.custom, customIconBinary)?.let {
return SuperDrawable(it)
}
}
val iconPack = IconPackChooser.getSelectedIconPack(context)
iconPack?.iconToResId(icon.standard.id)?.let { iconId ->
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable())
} ?: run {
return SuperDrawable(PatternIcon(context.resources).blankDrawable)
}
}
/**
* Build a custom [Drawable] from custom [icon]
*/
private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? {
val patternIcon = PatternIcon(resources)
val cipherKey = retrieveCipherKey()
if (cipherKey != null) {
val draw: Drawable? = customIconMap[icon.uuid]?.get()
if (draw == null) {
iconCustomBinary?.let { binaryFile ->
try {
var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(cipherKey))
bitmap?.let { bitmapIcon ->
bitmap = resize(bitmapIcon, patternIcon)
val createdDraw = BitmapDrawable(resources, bitmap)
customIconMap[icon.uuid] = WeakReference(createdDraw)
return createdDraw
}
} catch (e: Exception) {
customIconMap.remove(icon.uuid)
Log.e(TAG, "Unable to create the bitmap icon", e)
}
}
} else { } else {
ImageViewCompat.setImageTintList(imageView, null) return draw
} }
} }
return null
}
/**
* Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet)
* , then tint it with [tintColor] if needed
*/
private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tintColor: Int): Drawable {
val newCacheKey = CacheKey(iconId, width, true, tintColor)
var draw: Drawable? = standardIconMap[newCacheKey]?.get()
if (draw == null) {
try {
draw = ResourcesCompat.getDrawable(resources, iconId, null)
} catch (e: Exception) {
Log.e(TAG, "Can't get icon", e)
}
if (draw != null) {
standardIconMap[newCacheKey] = WeakReference(draw)
}
}
if (draw == null) {
draw = PatternIcon(resources).blankDrawable
}
draw.isFilterBitmap = false
return draw
}
/**
* Resize the custom icon to match the built in icons
*
* @param bitmap Bitmap to resize
* @return Bitmap resized
*/
private fun resize(bitmap: Bitmap, dimensionPattern: PatternIcon): Bitmap {
val width = bitmap.width
val height = bitmap.height
return if (width == dimensionPattern.width && height == dimensionPattern.height) {
bitmap
} else Bitmap.createScaledBitmap(bitmap, dimensionPattern.width, dimensionPattern.height, true)
}
/**
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
*/
fun assignDatabaseIcon(imageView: ImageView,
icon: IconImageDraw,
tintColor: Int = Color.WHITE) {
try {
val context = imageView.context
CoroutineScope(Dispatchers.IO).launch {
addToCustomCache(context.resources, icon)
withContext(Dispatchers.Main) {
val superDrawable = getIconSuperDrawable(context,
icon,
imageView.width,
tintColor)
imageView.setImageDrawable(superDrawable.drawable)
if (superDrawable.tintable) {
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
} else {
ImageViewCompat.setImageTintList(imageView, null)
}
}
}
} catch (e: Exception) {
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
}
} }
/** /**
* Utility method to assign a drawable to a RemoteView and tint it * Build a bitmap from a database [icon]
*/ */
fun assignDrawableToRemoteViews(superDrawable: SuperDrawable, fun getBitmapFromIcon(context: Context,
remoteViews: RemoteViews, icon: IconImageDraw,
imageId: Int, tintColor: Int = Color.BLACK): Bitmap? {
tintColor: Int = Color.BLACK) { try {
val bitmap = superDrawable.drawable.toBitmap() val superDrawable = getIconSuperDrawable(context,
// Tint bitmap if it's not a custom icon icon,
if (superDrawable.tintable && bitmap.isMutable) { 24,
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply { tintColor)
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN) val bitmap = superDrawable.drawable.toBitmap()
}) // Tint bitmap if it's not a custom icon
if (superDrawable.tintable && bitmap.isMutable) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
return bitmap
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to create bitmap from icon", e)
} }
remoteViews.setImageViewBitmap(imageId, bitmap) return null
} }
/** /**
* Utility method to assign a drawable to a icon and tint it * Simple method to init the cache with the custom icon and be much faster next time
*/ */
@RequiresApi(Build.VERSION_CODES.M) private fun addToCustomCache(resources: Resources, iconDraw: IconImageDraw) {
fun assignDrawableToIcon(superDrawable: SuperDrawable, val icon = iconDraw.getIconImageToDraw()
tintColor: Int = Color.BLACK): Icon { val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
val bitmap = superDrawable.drawable.toBitmap() if (customIconBinary != null
// Tint bitmap if it's not a custom icon && customIconBinary.dataExists()
if (superDrawable.tintable && bitmap.isMutable) { && !customIconMap.containsKey(icon.custom.uuid))
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply { getIconDrawable(resources, icon.custom, customIconBinary)
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
return Icon.createWithBitmap(bitmap)
} }
/** /**
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed * Clear a specific icon from the cache
*/ */
fun getIconSuperDrawable(context: Context, icon: IconImage, width: Int, tint: Boolean = false, tintColor: Int = Color.WHITE): SuperDrawable { fun clearFromCache(icon: IconImageCustom) {
return when (icon) { customIconMap.remove(icon.uuid)
is IconImageStandard -> { }
val resId = IconPackChooser.getSelectedIconPack(context)?.iconToResId(icon.iconId) ?: R.drawable.ic_blank_32dp
getIconSuperDrawable(context, resId, width, tint, tintColor) /**
} * Clear the cache of icons
is IconImageCustom -> { */
SuperDrawable(getIconDrawable(context.resources, icon)) fun clearCache() {
} standardIconMap.clear()
else -> { customIconMap.clear()
SuperDrawable(PatternIcon(context.resources).blankDrawable) }
}
/**
* Build a blankDrawable drawable
* @param res Resource to build the drawable
*/
private class PatternIcon(res: Resources) {
var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)
var width = -1
var height = -1
init {
width = res.getDimension(R.dimen.icon_size).toInt()
height = res.getDimension(R.dimen.icon_size).toInt()
blankDrawable.setBounds(0, 0, width, height)
} }
} }
/** /**
* Get the [SuperDrawable] IconImageStandard from [iconId] (cache, or build it and add it to the cache if not exists yet) * Utility class to prevent a custom icon to be tint
* , then [tint] it with [tintColor] if needed
*/ */
fun getIconSuperDrawable(context: Context, iconId: Int, width: Int, tint: Boolean, tintColor: Int): SuperDrawable { class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false)
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tint, tintColor), true)
}
/** /**
* Key class to retrieve a Drawable in the cache if it's tinted or not * Key class to retrieve a Drawable in the cache if it's tinted or not
@@ -161,189 +287,9 @@ class IconDrawableFactory {
} }
} }
/**
* Build a custom [Drawable] from custom [icon]
*/
private fun getIconDrawable(resources: Resources, icon: IconImageCustom): Drawable {
val patternIcon = PatternIcon(resources)
var draw: Drawable? = customIconMap[icon.uuid] as Drawable?
if (draw == null) {
var bitmap: Bitmap? = BitmapFactory.decodeByteArray(icon.imageData, 0, icon.imageData.size)
// Could not understand custom icon
bitmap?.let { bitmapIcon ->
bitmap = resize(bitmapIcon, patternIcon)
draw = BitmapDrawable(resources, bitmap)
customIconMap[icon.uuid] = draw
return draw!!
}
} else {
return draw!!
}
return patternIcon.blankDrawable
}
/**
* Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet)
* , then [tint] it with [tintColor] if needed
*/
private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tint: Boolean, tintColor: Int): Drawable {
val newCacheKey = CacheKey(iconId, width, tint, tintColor)
var draw: Drawable? = standardIconMap[newCacheKey] as Drawable?
if (draw == null) {
try {
draw = ResourcesCompat.getDrawable(resources, iconId, null)
} catch (e: Exception) {
Log.e(TAG, "Can't get icon", e)
}
if (draw != null) {
standardIconMap[newCacheKey] = draw
}
}
if (draw == null) {
draw = PatternIcon(resources).blankDrawable
}
draw.isFilterBitmap = false
return draw
}
/**
* Resize the custom icon to match the built in icons
*
* @param bitmap Bitmap to resize
* @return Bitmap resized
*/
private fun resize(bitmap: Bitmap, dimensionPattern: PatternIcon): Bitmap {
val width = bitmap.width
val height = bitmap.height
return if (width == dimensionPattern.width && height == dimensionPattern.height) {
bitmap
} else Bitmap.createScaledBitmap(bitmap, dimensionPattern.width, dimensionPattern.height, true)
}
/**
* Clear the cache of icons
*/
fun clearCache() {
standardIconMap.clear()
customIconMap.clear()
}
private class PatternIcon
/**
* Build a blankDrawable drawable
* @param res Resource to build the drawable
*/(res: Resources) {
var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)
var width = -1
var height = -1
init {
width = res.getDimension(R.dimen.icon_size).toInt()
height = res.getDimension(R.dimen.icon_size).toInt()
blankDrawable.setBounds(0, 0, width, height)
}
}
/**
* Utility class to prevent a custom icon to be tint
*/
class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false)
companion object { companion object {
private val TAG = IconDrawableFactory::class.java.name private val TAG = IconDrawableFactory::class.java.name
} }
} }
/**
* Assign a default database icon to an ImageView and tint it with [tintColor] if needed
*/
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory,
tintColor: Int = Color.WHITE) {
try {
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
iconFactory.assignDrawableToImageView(
iconFactory.getIconSuperDrawable(context,
selectedIconPack.defaultIconId,
width,
selectedIconPack.tintable(),
tintColor),
this,
selectedIconPack.tintable(),
tintColor)
}
} catch (e: Exception) {
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
}
}
/**
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
*/
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.WHITE) {
try {
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
iconFactory.assignDrawableToImageView(
iconFactory.getIconSuperDrawable(context,
icon,
width,
true,
tintColor),
this,
selectedIconPack.tintable(),
tintColor)
}
} catch (e: Exception) {
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
}
}
fun RemoteViews.assignDatabaseIcon(context: Context,
imageId: Int,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK) {
try {
iconFactory.assignDrawableToRemoteViews(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
this,
imageId,
tintColor)
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun createIconFromDatabaseIcon(context: Context,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK): Icon? {
try {
return iconFactory.assignDrawableToIcon(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
tintColor)
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}

View File

@@ -78,7 +78,7 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) {
// Build the list of icons // Build the list of icons
var num = 0 var num = 0
while (num <= NB_ICONS) { while (num < NB_ICONS) {
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp ) // To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
val resId = resources.getIdentifier( val resId = resources.getIdentifier(
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp", id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
@@ -134,7 +134,6 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) {
} }
companion object { companion object {
const val NB_ICONS = 69
private const val NB_ICONS = 68
} }
} }

View File

@@ -93,7 +93,7 @@ object IconPackChooser {
fun setSelectedIconPack(iconPackIdString: String?) { fun setSelectedIconPack(iconPackIdString: String?) {
for (iconPack in iconPackList) { for (iconPack in iconPackList) {
if (iconPack.id == iconPackIdString) { if (iconPack.id == iconPackIdString) {
Database.getInstance().drawFactory.clearCache() Database.getInstance().iconDrawableFactory.clearCache()
iconPackSelected = iconPack iconPackSelected = iconPack
break break
} }

View File

@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*

View File

@@ -22,26 +22,29 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryByte
import com.kunzisoft.keepass.utils.readEnum import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum import com.kunzisoft.keepass.utils.writeEnum
data class EntryAttachmentState(var attachment: Attachment, data class EntryAttachmentState(var attachment: Attachment,
var streamDirection: StreamDirection, var streamDirection: StreamDirection,
var downloadState: AttachmentState = AttachmentState.NULL, var downloadState: AttachmentState = AttachmentState.NULL,
var downloadProgression: Int = 0) : Parcelable { var downloadProgression: Int = 0,
var previewState: AttachmentState = AttachmentState.NULL) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()), parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryByte()),
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD, parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL, parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
parcel.readInt()) parcel.readInt(),
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL)
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(attachment, flags) parcel.writeParcelable(attachment, flags)
parcel.writeEnum(streamDirection) parcel.writeEnum(streamDirection)
parcel.writeEnum(downloadState) parcel.writeEnum(downloadState)
parcel.writeInt(downloadProgression) parcel.writeInt(downloadProgression)
parcel.writeEnum(previewState)
} }
override fun describeContents(): Int { override fun describeContents(): Int {
@@ -73,5 +76,5 @@ data class EntryAttachmentState(var attachment: Attachment,
} }
enum class AttachmentState { enum class AttachmentState {
NULL, START, IN_PROGRESS, COMPLETE, ERROR NULL, START, IN_PROGRESS, COMPLETE, CANCELED, ERROR
} }

View File

@@ -23,44 +23,28 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import kotlin.collections.ArrayList
class EntryInfo : Parcelable { class EntryInfo : NodeInfo {
var id: String = "" var id: String = ""
var title: String = ""
var icon: IconImage = IconImageStandard()
var username: String = "" var username: String = ""
var password: String = "" var password: String = ""
var creationTime: DateInstant = DateInstant()
var modificationTime: DateInstant = DateInstant()
var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
var url: String = "" var url: String = ""
var notes: String = "" var notes: String = ""
var customFields: List<Field> = ArrayList() var customFields: List<Field> = ArrayList()
var attachments: List<Attachment> = ArrayList() var attachments: List<Attachment> = ArrayList()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
constructor() constructor(): super()
private constructor(parcel: Parcel) { constructor(parcel: Parcel): super(parcel) {
id = parcel.readString() ?: id id = parcel.readString() ?: id
title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
username = parcel.readString() ?: username username = parcel.readString() ?: username
password = parcel.readString() ?: password password = parcel.readString() ?: password
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime
expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
url = parcel.readString() ?: url url = parcel.readString() ?: url
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(customFields, Field::class.java.classLoader)
@@ -73,15 +57,10 @@ class EntryInfo : Parcelable {
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeString(id) parcel.writeString(id)
parcel.writeString(title)
parcel.writeParcelable(icon, flags)
parcel.writeString(username) parcel.writeString(username)
parcel.writeString(password) parcel.writeString(password)
parcel.writeParcelable(creationTime, flags)
parcel.writeParcelable(modificationTime, flags)
parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags)
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(notes) parcel.writeString(notes)
parcel.writeArray(customFields.toTypedArray()) parcel.writeArray(customFields.toTypedArray())

View File

@@ -0,0 +1,36 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
class GroupInfo : NodeInfo {
var notes: String? = null
init {
icon.standard = IconImageStandard(FOLDER_ID)
}
constructor(): super()
constructor(parcel: Parcel): super(parcel) {
notes = parcel.readString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeString(notes)
}
companion object CREATOR : Parcelable.Creator<GroupInfo> {
override fun createFromParcel(parcel: Parcel): GroupInfo {
return GroupInfo(parcel)
}
override fun newArray(size: Int): Array<GroupInfo?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,32 @@
package com.kunzisoft.keepass.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readParcelable(Uri::class.java.classLoader)) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(masterPassword)
parcel.writeParcelable(keyFileUri, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,48 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
open class NodeInfo() : Parcelable {
var title: String = ""
var icon: IconImage = IconImage()
var creationTime: DateInstant = DateInstant()
var lastModificationTime: DateInstant = DateInstant()
var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
constructor(parcel: Parcel) : this() {
title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: lastModificationTime
expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeParcelable(icon, flags)
parcel.writeParcelable(creationTime, flags)
parcel.writeParcelable(lastModificationTime, flags)
parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<NodeInfo> {
override fun createFromParcel(parcel: Parcel): NodeInfo {
return NodeInfo(parcel)
}
override fun newArray(size: Int): Array<NodeInfo?> {
return arrayOfNulls(size)
}
}
}

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