Compare commits

..

132 Commits

Author SHA1 Message Date
J-Jamet
60ba058515 Merge branch 'release/2.9.13' 2021-02-15 12:47:48 +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
e6b33d60c3 Merge branch 'release/2.9.12' 2021-01-23 13:44:33 +01:00
J-Jamet
3fd06890d7 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-01-23 13:22:56 +01:00
J-Jamet
4af4ad7663 Fix show UUID key 2021-01-23 13:18:03 +01:00
J-Jamet
6ca8501e28 Keep current screen after theme change 2021-01-23 13:13:01 +01:00
J-Jamet
432b385f60 Fix orientation change in settings #872 2021-01-23 12:56:47 +01:00
Hosted Weblate
6cebdefa4a Merge branch 'origin/develop' into Weblate. 2021-01-23 12:48:39 +01:00
J-Jamet
bc665eb83d Fix orientation change in settings #872 2021-01-23 12:16:18 +01:00
J-Jamet
cb187300fe Deactivate GiB #851 2021-01-23 11:21:38 +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
f34e007ecd Add kibibyte and gibibyte #851 2021-01-22 16:07:50 +01:00
J-Jamet
3b6ad080b4 Update CHANGELOG 2021-01-22 15:46:30 +01:00
J-Jamet
9919e90ba5 Change memory unit to MiB 2021-01-22 15:37:50 +01:00
WaldiS
f4af44925b Translated using Weblate (Polish)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-01-21 21:45:29 +01:00
J-Jamet
4bb366b568 Capture exception in IO action task 2021-01-21 14:38:34 +01:00
J-Jamet
7e7ab4ce19 Catch exception when check database info 2021-01-21 14:32:11 +01:00
J-Jamet
4d833d25ce Add IME_FLAG_NO_PERSONALIZED_LEARNING options #642 2021-01-21 14:16:13 +01:00
J-Jamet
a9c508ecd9 Fix back appearance setting #865 2021-01-21 13:36:50 +01:00
J-Jamet
ef4dbb8fdb Fix auto open biometric prompt #862 2021-01-21 12:46:43 +01:00
J-Jamet
9eb66face5 Fix reload exception 2021-01-20 12:46:08 +01:00
J-Jamet
3fd13f3e3b Try to fix decodeHex method conflict in some devices 2021-01-20 11:42:09 +01:00
Kornelijus Tvarijanavičius
319c9cad4b Translated using Weblate (Lithuanian)
Currently translated at 15.1% (77 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2021-01-20 11:32:13 +01:00
nautilusx
c12297c98d Translated using Weblate (German)
Currently translated at 98.8% (501 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-01-20 11:32:13 +01:00
J-Jamet
7c38361844 Fix OTP token type #863 2021-01-18 16:48:57 +01:00
J-Jamet
559554a975 Upgrade to 2.9.12 2021-01-18 16:47:48 +01:00
Darin Avdeyeva
7e2ffa2124 Translated using Weblate (Russian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-01-17 19:40:00 +01:00
Milo Ivir
66dbac4bb2 Translated using Weblate (Croatian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-01-17 15:28:44 +01:00
Carlos Pinto
8b6a843a85 Translated using Weblate (Portuguese (Portugal))
Currently translated at 90.7% (460 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2021-01-17 15:28:43 +01:00
HARADA Hiroyuki
976cff2751 Translated using Weblate (Japanese)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-01-17 15:28:43 +01:00
zeritti
f7c30fa8eb Translated using Weblate (Czech)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-01-17 15:28:42 +01:00
J-Jamet
7757c8218b Merge tag '2.9.11' into develop
2.9.11
2021-01-16 19:09:33 +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
147 changed files with 4109 additions and 1302 deletions

View File

@@ -1,3 +1,21 @@
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)
* Fix OTP token type #863
* Fix auto open biometric prompt #862
* Fix back appearance setting #865
* Fix orientation change in settings #872
* Change memory unit to MiB #851
* Small changes #642
KeePassDX(2.9.11) KeePassDX(2.9.11)
* Add Keyfile XML version 2 (fix hex) #844 * Add Keyfile XML version 2 (fix hex) #844
* Fix hex Keyfile #861 * Fix hex Keyfile #861

View File

@@ -12,12 +12,12 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 30 targetSdkVersion 30
versionCode = 55 versionCode = 57
versionName = "2.9.11" versionName = "2.9.13"
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 +51,7 @@ 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_Purple\",\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
} }
pro { pro {
@@ -70,7 +70,7 @@ 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_Red\",\"KeepassDXStyle_Purple\",\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ] manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
} }
@@ -82,6 +82,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
@@ -120,14 +124,15 @@ 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-collections:commons-collections:3.2.2'
// Apache Commons Codec implementation 'commons-io:commons-io:2.8.0'
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,156 @@
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.BinaryAttachment
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 java.lang.Exception
import java.security.MessageDigest
class BinaryAttachmentTest {
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, binaryAttachment: BinaryAttachment) {
context.assets.open(asset).use { assetInputStream ->
binaryAttachment.getOutputDataStream(loadedKey).use { binaryOutputStream ->
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
binaryOutputStream.write(buffer)
}
}
}
}
@Test
fun testSaveTextInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
assertEquals("Save text binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save text binary MD5 failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testSaveImageInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
assertEquals("Save image binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save image binary failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testCompressText() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(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.length, binaryB.length)
assertEquals("Compress text MD5 failed.", binaryA.md5(), binaryB.md5())
binaryB.decompress(loadedKey)
assertEquals("Decompress text length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress text MD5 failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testCompressImage() {
val binaryA = BinaryAttachment(fileA)
var binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(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.length, binaryA.length)
assertEquals("Compress image failed.", binaryA.md5(), binaryA.md5())
binaryB = BinaryAttachment(fileB, true)
binaryB.decompress(loadedKey)
assertEquals("Decompress image length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress image failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testReadText() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_TEXT_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
@Test
fun testReadImage() {
val binaryA = BinaryAttachment(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()
}
}
private fun BinaryAttachment.md5(): String {
val md = MessageDigest.getInstance("MD5")
return this.getInputDataStream(loadedKey).use { fis ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
generateSequence {
when (val bytesRead = fis.read(buffer)) {
-1 -> null
else -> bytesRead
}
}.forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
md.digest().joinToString("") { "%02x".format(it) }
}
}
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,12 @@
<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.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 +178,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 +216,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

@@ -51,22 +51,19 @@ 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 +126,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 +154,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 +221,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)
}
} }
} }
} }
@@ -349,7 +350,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
@@ -57,22 +56,22 @@ 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.*
@@ -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)
@@ -198,14 +198,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?.drawFactory
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
@@ -236,51 +236,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 +293,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 +354,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,10 +470,12 @@ 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)
} }
} }
@@ -572,6 +528,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 +540,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 ->
val tempAttachment = tempAttachmentState.attachment
mDatabase?.binaryPool?.let { binaryPool -> mDatabase?.binaryPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(it)) { if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
mDatabase?.removeAttachmentIfNotUsed(it) mDatabase?.removeAttachmentIfNotUsed(tempAttachment)
} }
} }
} }
@@ -619,12 +596,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 +671,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 -> {
@@ -787,6 +790,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

@@ -26,10 +26,8 @@ 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
@@ -41,6 +39,7 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha
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
@@ -49,6 +48,7 @@ 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,10 +74,9 @@ 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: View.OnClickListener? = null
var setOnEditCustomField: ((Field) -> Unit)? = null var setOnEditCustomField: ((Field) -> Unit)? = null
@@ -86,6 +84,7 @@ class EntryEditFragment: StylishFragment() {
// 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
@@ -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
} }
@@ -283,41 +275,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
@@ -502,9 +473,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 +503,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 +511,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

@@ -52,12 +52,13 @@ 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
@@ -199,8 +200,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 -> {
@@ -330,9 +331,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,10 +339,7 @@ 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) {
@@ -353,11 +349,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} }
} }
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)

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
@@ -53,37 +53,37 @@ 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.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, IconPickerDialogFragment.IconPickerListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
ListNodesFragment.NodeClickListener, ListNodesFragment.NodeClickListener,
ListNodesFragment.NodesActionMenuListener, ListNodesFragment.NodesActionMenuListener,
DeleteNodesDialogFragment.DeleteNodeListener, DeleteNodesDialogFragment.DeleteNodeListener,
@@ -345,13 +345,18 @@ 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() startActivity(intent)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
} else {
this.showActionErrorIfNeeded(result)
finish()
}
} }
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionErrorIfNeeded(result)
finishNodeAction() finishNodeAction()
@@ -700,6 +705,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 +783,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.build(mOldGroupToUpdate!!.getGroupInfo())
.show(supportFragmentManager, .show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP) GroupEditDialogFragment.TAG_CREATE_GROUP)
} }
@@ -1031,19 +1069,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 +1099,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,9 +1115,8 @@ 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
} }

View File

@@ -0,0 +1,116 @@
/*
* 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.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 kotlinx.android.synthetic.main.activity_image_viewer.*
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 imageView: ImageView = findViewById(R.id.image_viewer_image)
val progressView: View = findViewById(R.id.image_viewer_progress)
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryAttachment.length)
Attachment.loadBitmap(attachment, Database.getInstance().loadedCipherKey) { 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, image_viewer_container) {
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

@@ -56,14 +56,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
@@ -236,15 +236,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 +250,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)
@@ -534,8 +531,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 +540,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) {
@@ -161,17 +160,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 +178,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 +243,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 +258,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,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,39 @@ 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.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.icons.assignDatabaseIcon 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(), IconPickerDialogFragment.IconPickerListener {
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 +72,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 +81,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,46 +102,48 @@ 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 (mEditGroupDialogAction === CREATION)
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) { mGroupInfo.notes = ""
nameGroup = getString(KEY_NAME) if (containsKey(KEY_GROUP_INFO)) {
iconGroup = getParcelable(KEY_ICON) mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
} }
} }
} }
// 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") IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
} }
@@ -150,55 +160,87 @@ 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()
} }
} }
} }
} }
fun getExpiryTime(): DateInstant {
retrieveGroupInfoFromViews()
return mGroupInfo.expiryTime
}
fun setExpiryTime(expiryTime: DateInstant) {
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() { private fun assignIconView() {
if (mDatabase?.drawFactory != null && iconGroup != null) { if (mDatabase?.drawFactory != null) {
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor) iconButtonView.assignDatabaseIcon(mDatabase?.drawFactory!!, mGroupInfo.icon, iconColor)
} }
} }
override fun iconPicked(bundle: Bundle) { override fun iconPicked(bundle: Bundle) {
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle) mGroupInfo.icon = IconPickerDialogFragment.getIconStandardFromBundle(bundle) ?: mGroupInfo.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 build(): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()
@@ -208,11 +250,10 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
return fragment return fragment
} }
fun build(group: Group): GroupEditDialogFragment { fun build(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

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

@@ -29,10 +29,7 @@ import android.view.MotionEvent
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.AdapterView import android.widget.*
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@@ -57,6 +54,7 @@ class SetOTPDialogFragment : DialogFragment() {
private var mOtpElement: OtpElement = OtpElement() private var mOtpElement: OtpElement = OtpElement()
private var otpTypeMessage: TextView? = null
private var otpTypeSpinner: Spinner? = null private var otpTypeSpinner: Spinner? = null
private var otpTokenTypeSpinner: Spinner? = null private var otpTokenTypeSpinner: Spinner? = null
private var otpSecretContainer: TextInputLayout? = null private var otpSecretContainer: TextInputLayout? = null
@@ -74,6 +72,8 @@ class SetOTPDialogFragment : DialogFragment() {
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
private var mManualEvent = false private var mManualEvent = false
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus -> private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
@@ -134,6 +134,7 @@ class SetOTPDialogFragment : DialogFragment() {
activity?.let { activity -> activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup? val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type) otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type) otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label) otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
@@ -183,23 +184,23 @@ class SetOTPDialogFragment : DialogFragment() {
// HOTP / TOTP Type selection // HOTP / TOTP Type selection
val otpTypeArray = OtpType.values() val otpTypeArray = OtpType.values()
otpTypeAdapter = ArrayAdapter<OtpType>(activity, otpTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, otpTypeArray).apply { android.R.layout.simple_spinner_item, otpTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
otpTypeSpinner?.adapter = otpTypeAdapter otpTypeSpinner?.adapter = otpTypeAdapter
// Otp Token type selection // Otp Token type selection
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues() mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
hotpTokenTypeAdapter = ArrayAdapter(activity, hotpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply { android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
// Proprietary only on closed and full version // Proprietary only on closed and full version
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues( mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION) BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
totpTokenTypeAdapter = ArrayAdapter(activity, totpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply { android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
otpTokenTypeAdapter = hotpTokenTypeAdapter otpTokenTypeAdapter = hotpTokenTypeAdapter
@@ -207,7 +208,7 @@ class SetOTPDialogFragment : DialogFragment() {
// OTP Algorithm // OTP Algorithm
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values() val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity, otpAlgorithmAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply { android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
@@ -372,24 +373,40 @@ class SetOTPDialogFragment : DialogFragment() {
} }
private fun upgradeTokenType() { private fun upgradeTokenType() {
val tokenType = mOtpElement.tokenType
when (mOtpElement.type) { when (mOtpElement.type) {
OtpType.HOTP -> { OtpType.HOTP -> {
otpPeriodContainer?.visibility = View.GONE otpPeriodContainer?.visibility = View.GONE
otpCounterContainer?.visibility = View.VISIBLE otpCounterContainer?.visibility = View.VISIBLE
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType mHotpTokenTypeArray?.let { otpTokenTypeArray ->
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType)) defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
}
} }
OtpType.TOTP -> { OtpType.TOTP -> {
otpPeriodContainer?.visibility = View.VISIBLE otpPeriodContainer?.visibility = View.VISIBLE
otpCounterContainer?.visibility = View.GONE otpCounterContainer?.visibility = View.GONE
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType mTotpTokenTypeArray?.let { otpTokenTypeArray ->
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType)) defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
}
} }
} }
} }
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
tokenType: OtpTokenType,
defaultTokenType: OtpTokenType) {
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
otpTypeMessage?.visibility = View.GONE
tokenType
} else {
otpTypeMessage?.visibility = View.VISIBLE
defaultTokenType
}
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
}
private fun upgradeParameters() { private fun upgradeParameters() {
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values() otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
.indexOf(mOtpElement.algorithm)) .indexOf(mOtpElement.algorithm))

View File

@@ -60,6 +60,9 @@ abstract class LockingActivity : SpecialModeActivity() {
private set private set
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState != null if (savedInstanceState != null
@@ -84,8 +87,6 @@ abstract class LockingActivity : SpecialModeActivity() {
} }
mExitLock = false mExitLock = false
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@@ -66,6 +66,7 @@ object Stylish {
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_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
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

@@ -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,16 +31,22 @@ 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.Attachment
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.view.expand
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
private var mTitleColor: Int private var mTitleColor: Int
@@ -62,6 +68,37 @@ 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
Attachment.loadBitmap(entryAttachmentState.attachment, binaryCipherKey) { 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.binaryAttachment.isCorrupted) {
@@ -77,7 +114,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
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.binaryAttachment.length)
holder.binaryFileCompression.apply { holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) { if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources) text = CompressionAlgorithm.GZip.getName(context.resources)
@@ -105,6 +142,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 +152,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 +160,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 +179,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

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

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

@@ -34,7 +34,12 @@ class IOActionTask<T>(
mainScope.launch { mainScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async { val asyncResult: Deferred<T?> = async {
action.invoke() try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await()) afterActionDatabaseListener?.invoke(asyncResult.await())

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
@@ -162,8 +162,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
disconnect() disconnect()
} }
} ?: run { } ?: run {
connect(databaseUri)
this.mAutoOpenPrompt = autoOpenPrompt this.mAutoOpenPrompt = autoOpenPrompt
connect(databaseUri)
} }
} else { } else {
disconnect() disconnect()

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,7 +33,10 @@ 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))
} }
@@ -43,9 +45,9 @@ class ReloadDatabaseRunnable(private val context: Context,
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 +55,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

@@ -19,9 +19,12 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.graphics.Bitmap
import android.graphics.BitmapFactory
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.BinaryAttachment
import kotlinx.coroutines.*
data class Attachment(var name: String, data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable { var binaryAttachment: BinaryAttachment) : Parcelable {
@@ -65,5 +68,28 @@ data class Attachment(var name: String,
override fun newArray(size: Int): Array<Attachment?> { override fun newArray(size: Int): Array<Attachment?> {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
fun loadBitmap(attachment: Attachment,
binaryCipherKey: Database.LoadedKey?,
actionOnFinish: (Bitmap?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<Bitmap?> = async {
runCatching {
binaryCipherKey?.let { binaryKey ->
var bitmap: Bitmap?
attachment.binaryAttachment.getUnGzipInputDataStream(binaryKey).use { bitmapInputStream ->
bitmap = BitmapFactory.decodeStream(bitmapInputStream)
}
bitmap
}
}.getOrNull()
}
withContext(Dispatchers.Main) {
actionOnFinish(asyncResult.await())
}
}
}
}
} }
} }

View File

@@ -41,12 +41,17 @@ 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 javax.crypto.spec.IvParameterSpec
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -74,6 +79,19 @@ class Database {
var loadTimestamp: Long? = null var loadTimestamp: Long? = null
private set private set
/**
* Cipher key regenerated when the database is loaded and closed
* 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
}
val iconFactory: IconImageFactory val iconFactory: IconImageFactory
get() { get() {
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory() return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
@@ -320,12 +338,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 +398,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 +425,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 +434,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 +465,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)
} }
} }
@@ -608,7 +658,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

View File

@@ -346,12 +346,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)
@@ -427,7 +421,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
@@ -467,7 +461,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
notes = newEntryInfo.notes notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool -> database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments) newEntryInfo.attachments.forEach { attachment ->
putAttachment(attachment, binaryPool)
}
} }
database?.stopManageEntry(this) database?.stopManageEntry(this)

View File

@@ -29,6 +29,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard 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
@@ -232,6 +233,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)
@@ -391,6 +400,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

@@ -21,23 +21,33 @@ package com.kunzisoft.keepass.database.element.database
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes 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.io.*
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
class BinaryAttachment : Parcelable { class BinaryAttachment : Parcelable {
private var dataFile: File? = null private var dataFile: File? = null
var length: Long = 0
private set
var isCompressed: Boolean = false var isCompressed: Boolean = false
private set private set
var isProtected: Boolean = false var isProtected: Boolean = false
private set private set
var isCorrupted: Boolean = false var isCorrupted: Boolean = false
// Cipher to encrypt temp file
fun length(): Long { private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
return dataFile?.length() ?: 0 private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
}
/** /**
* Empty protected binary * Empty protected binary
@@ -46,6 +56,7 @@ class BinaryAttachment : Parcelable {
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) { constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
this.dataFile = dataFile this.dataFile = dataFile
this.length = 0
this.isCompressed = compressed this.isCompressed = compressed
this.isProtected = protected this.isProtected = protected
} }
@@ -54,58 +65,77 @@ class BinaryAttachment : Parcelable {
parcel.readString()?.let { parcel.readString()?.let {
dataFile = File(it) dataFile = File(it)
} }
length = parcel.readLong()
isCompressed = parcel.readByte().toInt() != 0 isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0 isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0 isCorrupted = parcel.readByte().toInt() != 0
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getInputDataStream(): InputStream { fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return buildInputStream(dataFile!!, cipherKey)
}
@Throws(IOException::class)
fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return buildOutputStream(dataFile!!, cipherKey)
}
@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)
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
return when { return when {
length() > 0 -> FileInputStream(dataFile!!) 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)) else -> ByteArrayInputStream(ByteArray(0))
} }
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getUnGzipInputDataStream(): InputStream { private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
return if (isCompressed)
GZIPInputStream(getInputDataStream())
else
getInputDataStream()
}
@Throws(IOException::class)
fun getOutputDataStream(): OutputStream {
return when { return when {
dataFile != null -> FileOutputStream(dataFile!!) 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") else -> throw IOException("Unable to write in an unknown file")
} }
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getGzipOutputDataStream(): OutputStream { fun compress(cipherKey: Database.LoadedKey) {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream())
} else {
getOutputDataStream()
}
}
@Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile -> dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file // To compress, create a new binary with file
if (!isCompressed) { if (!isCompressed) {
// Encrypt the new gzipped temp file
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream -> getInputDataStream(cipherKey).use { inputStream ->
getInputDataStream().use { inputStream -> GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
inputStream.readBytes(bufferSize) { buffer -> inputStream.readAllBytes { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
} }
} }
} }
// Remove unGzip file // Remove ungzip file
if (concreteDataFile.delete()) { if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) { if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression // Harmonize with database compression
@@ -117,13 +147,14 @@ class BinaryAttachment : Parcelable {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { fun decompress(cipherKey: Database.LoadedKey) {
dataFile?.let { concreteDataFile -> dataFile?.let { concreteDataFile ->
if (isCompressed) { if (isCompressed) {
// Encrypt the new ungzipped temp file
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
FileOutputStream(fileBinaryDecompress).use { outputStream -> getUnGzipInputDataStream(cipherKey).use { inputStream ->
getUnGzipInputDataStream().use { inputStream -> buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
inputStream.readBytes(bufferSize) { buffer -> inputStream.readAllBytes { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
} }
} }
@@ -170,6 +201,7 @@ class BinaryAttachment : Parcelable {
result = 31 * result + if (isProtected) 1 else 0 result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0 result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode() result = 31 * result + dataFile!!.hashCode()
result = 31 * result + length.hashCode()
return result return result
} }
@@ -183,11 +215,31 @@ class BinaryAttachment : Parcelable {
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(dataFile?.absolutePath) dest.writeString(dataFile?.absolutePath)
dest.writeLong(length)
dest.writeByte((if (isCompressed) 1 else 0).toByte()) dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte()) dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte()) dest.writeByte((if (isCorrupted) 1 else 0).toByte())
} }
/**
* Custom OutputStream to calculate the size of binary file
*/
private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
init {
length = 0
}
override fun beforeWrite(n: Int) {
super.beforeWrite(n)
length = byteCount
}
override fun close() {
super.close()
length = byteCount
}
}
companion object { companion object {
private val TAG = BinaryAttachment::class.java.name private val TAG = BinaryAttachment::class.java.name

View File

@@ -281,7 +281,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

@@ -126,6 +126,7 @@ 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 = iconFactory.folderIcon
@@ -212,8 +213,10 @@ 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)
} }
@@ -223,7 +226,9 @@ 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)
} }
@@ -451,7 +456,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return if (hashString != null return if (hashString != null
&& checkKeyFileHash(dataString, hashString)) { && checkKeyFileHash(dataString, hashString)) {
Log.i(TAG, "Successful key file hash check.") Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString) Hex.decodeHex(dataString.toCharArray())
} else { } else {
Log.e(TAG, "Unable to check the hash of the key file.") Log.e(TAG, "Unable to check the hash of the key file.")
null null
@@ -477,7 +482,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
digest = MessageDigest.getInstance("SHA-256") digest = MessageDigest.getInstance("SHA-256")
digest?.reset() digest?.reset()
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key. // hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = digest.digest(Hex.decodeHex(data)) val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4) .copyOfRange(0, 4)
.toHexString() .toHexString()
success = dataDigest == hash success = dataDigest == hash
@@ -708,7 +713,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,6 +20,7 @@
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.IconImageFactory
@@ -84,13 +85,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
@@ -140,7 +141,7 @@ abstract class DatabaseVersioned<
when (keyData.size) { when (keyData.size) {
32 -> return keyData 32 -> return keyData
64 -> try { 64 -> try {
return Hex.decodeHex(String(keyData)) return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) { } catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data // Key is not base 64, treat it as binary data
} }
@@ -383,6 +384,12 @@ abstract class DatabaseVersioned<
return true return true
} }
/**
* 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
companion object { companion object {
private const val TAG = "DatabaseVersioned" private const val TAG = "DatabaseVersioned"

View File

@@ -316,7 +316,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
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 += binaryPool[poolId]?.length ?: 0
} }
return size return size
} }

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,9 @@ 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
// Import all nodes // Import all nodes
var newGroup: GroupKDB? = null var newGroup: GroupKDB? = null
@@ -192,12 +197,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 +216,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 +231,7 @@ class DatabaseInputKDB(cacheDirectory: File)
if (iconId == -1) { if (iconId == -1) {
iconId = 0 iconId = 0
} }
entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId) entry.icon = mDatabase.iconFactory.getIcon(iconId)
} }
} }
0x0004 -> { 0x0004 -> {
@@ -255,7 +260,7 @@ class DatabaseInputKDB(cacheDirectory: File)
} }
0x0007 -> { 0x0007 -> {
newGroup?.let { group -> newGroup?.let { group ->
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) group.icon = mDatabase.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} ?: } ?:
newEntry?.let { entry -> newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false) entry.password = cipherInputStream.readBytesToString(fieldSize,false)
@@ -300,11 +305,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.buildNewBinary(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 +320,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 +343,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 +381,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,6 +26,7 @@ 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.BinaryAttachment
@@ -96,20 +97,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
} }
} }
@@ -273,8 +278,10 @@ class DatabaseInputKDBX(cacheDirectory: File)
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.buildNewBinary(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)
} }
} }
@@ -1009,14 +1016,16 @@ class DatabaseInputKDBX(cacheDirectory: File)
// Build the new binary and compress // Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId) val binaryAttachment = mDatabase.buildNewBinary(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 +1092,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,13 @@ 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
val binaryCipherKey = mDatabaseKDB.loadedCipherKey
?: throw DatabaseOutputException("Unable to retrieve cipher key to write binaries")
mDatabaseKDB.doForEachEntryInIndex { entry -> mDatabaseKDB.doForEachEntryInIndex { entry ->
val peo = EntryOutputKDB(entry, os) EntryOutputKDB(entry, outputStream, binaryCipherKey).output()
try {
peo.output()
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output an entry.", e)
}
} }
} }

View File

@@ -32,7 +32,6 @@ 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
@@ -140,12 +139,14 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
database.binaryPool.doForEachOrderedBinary { _, keyBinary -> database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary val protectedBinary = keyBinary.binary
val binaryCipherKey = database.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to write binaries")
// Force decompression to add binary in header // Force decompression to add binary in header
protectedBinary.decompress() protectedBinary.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(protectedBinary.length + 1))
// Write protected flag // Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) { if (protectedBinary.isProtected) {
@@ -153,8 +154,8 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
dataOutputStream.writeByte(flag) dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream -> protectedBinary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.readAllBytes { buffer ->
dataOutputStream.write(buffer) dataOutputStream.write(buffer)
} }
} }
@@ -473,7 +474,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 +483,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)))
} }
} }
@@ -502,13 +503,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
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 val binary = keyBinary.binary
if (binary.length() > 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)
} }
// Write the XML // Write the XML
binary.getInputDataStream().use { inputStream -> val binaryCipherKey = mDatabaseKDBX.loadedCipherKey
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> ?: throw IOException("Unable to retrieve cipher key to write binaries")
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
} }
} }
@@ -589,7 +592,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)
@@ -718,17 +721,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
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,9 @@
*/ */
package com.kunzisoft.keepass.database.file.output package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.database.DatabaseKDB 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 +29,91 @@ 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.iconId)))
// 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)) mOutputStream.write(BINARY_DATA_FIELD_TYPE)
val binaryData = mEntry.binaryData
// Binary description val binaryDataLength = binaryData?.length ?: 0L
mOutputStream.write(BINARY_DESC_FIELD_TYPE) // Write data length
length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong() mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
// Write data
// Binary if (binaryDataLength > 0) {
mOutputStream.write(BINARY_DATA_FIELD_TYPE) binaryData?.getInputDataStream(mCipherKey).use { inputStream ->
val binaryData = mEntry.binaryData inputStream?.readAllBytes { buffer ->
val binaryDataLength = binaryData?.length() ?: 0L mOutputStream.write(buffer)
// Write data length }
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength))) inputStream?.close()
// Write data
if (binaryDataLength > 0) {
binaryData?.getInputDataStream().use { inputStream ->
inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
length += buffer.size
mOutputStream.write(buffer)
} }
inputStream?.close()
} }
}
// 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)
@@ -144,29 +139,27 @@ class EntryOutputKDB
companion object { companion object {
// Constants // Constants
val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1) private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) private val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) private val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) private val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) private val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) private val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10) private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11) private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12) private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13) private val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14) private val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16)) 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(4)) private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) private val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
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.iconId)))
// 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

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

@@ -29,19 +29,22 @@ 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("", BinaryAttachment()),
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
class GroupInfo : NodeInfo {
var notes: String? = null
init {
icon = IconImageStandard(FOLDER)
}
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,49 @@
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
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
open class NodeInfo() : Parcelable {
var title: String = ""
var icon: IconImage = IconImageStandard()
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)
}
}
}

View File

@@ -138,7 +138,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun setHexSecret(secret: String) { fun setHexSecret(secret: String) {
if (secret.isNotEmpty()) if (secret.isNotEmpty())
otpModel.secret = Hex.decodeHex(secret) otpModel.secret = Hex.decodeHex(secret.toCharArray())
else else
throw IllegalArgumentException() throw IllegalArgumentException()
} }
@@ -210,13 +210,13 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun isValidBase32(secret: String): Boolean { fun isValidBase32(secret: String): Boolean {
val secretChars = replaceBase32Chars(secret) val secretChars = replaceBase32Chars(secret)
return secret.isNotEmpty() return secret.isNotEmpty()
&& (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secretChars)) && (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}=*|[A-Z2-7]{4}=*|[A-Z2-7]{5}=*|[A-Z2-7]{7}=*)?$", secretChars))
} }
fun isValidBase64(secret: String): Boolean { fun isValidBase64(secret: String): Boolean {
// TODO replace base 64 chars // TODO replace base 64 chars
return secret.isNotEmpty() return secret.isNotEmpty()
&& (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret)) && (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}=*|[A-Za-z0-9+/]{3}=*)?$", secret))
} }
fun replaceBase32Chars(parameter: String): String { fun replaceBase32Chars(parameter: String): String {

View File

@@ -354,9 +354,15 @@ object OtpEntryFields {
return false return false
} }
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = matcher.group(2)?.let { matcher.group(2)?.let { secondMatcher ->
OtpTokenType.getFromString(it) try {
} ?: OtpTokenType.RFC6238 otpElement.digits = secondMatcher.toInt()
} catch (e: NumberFormatException) {
otpElement.digits = OTP_DEFAULT_DIGITS
otpElement.tokenType = OtpTokenType.getFromString(secondMatcher)
}
}
} }
} catch (exception: Exception) { } catch (exception: Exception) {
return false return false

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.notifications package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context

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.notifications package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ContentResolver import android.content.ContentResolver
@@ -29,15 +29,14 @@ import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
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.stream.readBytes import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.BufferedInputStream
import java.util.* import java.util.*
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@@ -100,12 +99,15 @@ class AttachmentFileNotificationService: LockNotificationService() {
when(intent?.action) { when(intent?.action) {
ACTION_ATTACHMENT_FILE_START_UPLOAD -> { ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
actionUploadOrDownload(downloadFileUri, actionStartUploadOrDownload(downloadFileUri,
intent, intent,
StreamDirection.UPLOAD) StreamDirection.UPLOAD)
} }
ACTION_ATTACHMENT_FILE_STOP_UPLOAD -> {
actionStopUpload()
}
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> { ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
actionUploadOrDownload(downloadFileUri, actionStartUploadOrDownload(downloadFileUri,
intent, intent,
StreamDirection.DOWNLOAD) StreamDirection.DOWNLOAD)
} }
@@ -215,15 +217,22 @@ class AttachmentFileNotificationService: LockNotificationService() {
setDeleteIntent(pendingDeleteIntent) setDeleteIntent(pendingDeleteIntent)
setOngoing(false) setOngoing(false)
} }
AttachmentState.CANCELED -> {
setContentText(getString(R.string.download_canceled))
setDeleteIntent(pendingDeleteIntent)
setOngoing(false)
}
AttachmentState.ERROR -> { AttachmentState.ERROR -> {
setContentText(getString(R.string.error_file_not_create)) setContentText(getString(R.string.error_file_not_create))
setDeleteIntent(pendingDeleteIntent)
setOngoing(false) setOngoing(false)
} }
} }
} }
when (attachmentNotification.entryAttachmentState.downloadState) { when (attachmentNotification.entryAttachmentState.downloadState) {
AttachmentState.ERROR, AttachmentState.COMPLETE,
AttachmentState.COMPLETE -> { AttachmentState.CANCELED,
AttachmentState.ERROR -> {
stopForeground(false) stopForeground(false)
notificationManager?.notify(attachmentNotification.notificationId, builder.build()) notificationManager?.notify(attachmentNotification.notificationId, builder.build())
} else -> { } else -> {
@@ -234,6 +243,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
override fun onDestroy() { override fun onDestroy() {
attachmentNotificationList.forEach { attachmentNotification -> attachmentNotificationList.forEach { attachmentNotification ->
attachmentNotification.attachmentFileAction?.cancel()
attachmentNotification.attachmentFileAction?.listener = null attachmentNotification.attachmentFileAction?.listener = null
notificationManager?.cancel(attachmentNotification.notificationId) notificationManager?.cancel(attachmentNotification.notificationId)
} }
@@ -262,10 +272,10 @@ class AttachmentFileNotificationService: LockNotificationService() {
} }
} }
private fun actionUploadOrDownload(downloadFileUri: Uri?, private fun actionStartUploadOrDownload(fileUri: Uri?,
intent: Intent, intent: Intent,
streamDirection: StreamDirection) { streamDirection: StreamDirection) {
if (downloadFileUri != null if (fileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) { && intent.hasExtra(ATTACHMENT_KEY)) {
try { try {
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment -> intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
@@ -273,7 +283,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId } val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1 ?.notificationId ?: notificationId) + 1
val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection) val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState) val attachmentNotification = AttachmentNotification(fileUri, nextNotificationId, entryAttachmentState)
// Add action to the list on start // Add action to the list on start
attachmentNotificationList.add(attachmentNotification) attachmentNotificationList.add(attachmentNotification)
@@ -286,11 +296,24 @@ class AttachmentFileNotificationService: LockNotificationService() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to upload/download $downloadFileUri", e) Log.e(TAG, "Unable to upload/download $fileUri", e)
} }
} }
} }
private fun actionStopUpload() {
try {
// Stop each upload
attachmentNotificationList.filter {
it.entryAttachmentState.streamDirection == StreamDirection.UPLOAD
}.forEach {
it.attachmentFileAction?.cancel()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to stop upload", e)
}
}
private class AttachmentFileAction( private class AttachmentFileAction(
private val attachmentNotification: AttachmentNotification, private val attachmentNotification: AttachmentNotification,
private val contentResolver: ContentResolver) { private val contentResolver: ContentResolver) {
@@ -307,8 +330,6 @@ class AttachmentFileNotificationService: LockNotificationService() {
// on pre execute // on pre execute
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
TimeoutHelper.temporarilyDisableTimeout()
attachmentNotification.attachmentFileAction = this@AttachmentFileAction attachmentNotification.attachmentFileAction = this@AttachmentFileAction
attachmentNotification.entryAttachmentState.apply { attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.START downloadState = AttachmentState.START
@@ -319,70 +340,81 @@ class AttachmentFileNotificationService: LockNotificationService() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// on Progress with thread // on Progress with thread
val asyncResult: Deferred<Boolean> = async { val asyncAction = launch {
var progressResult = true attachmentNotification.entryAttachmentState.apply {
try { try {
attachmentNotification.entryAttachmentState.apply { downloadState = AttachmentState.IN_PROGRESS
downloadState = AttachmentState.IN_PROGRESS
when (streamDirection) { when (streamDirection) {
StreamDirection.UPLOAD -> { StreamDirection.UPLOAD -> {
uploadToDatabase( uploadToDatabase(
attachmentNotification.uri, attachmentNotification.uri,
attachment.binaryAttachment, attachment.binaryAttachment,
contentResolver, 1024) { percent -> contentResolver, 1024,
publishProgress(percent) { // Cancellation
downloadState == AttachmentState.CANCELED
}
) { percent ->
publishProgress(percent)
}
}
StreamDirection.DOWNLOAD -> {
downloadFromDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
contentResolver, 1024) { percent ->
publishProgress(percent)
}
} }
} }
StreamDirection.DOWNLOAD -> { } catch (e: Exception) {
downloadFromDatabase( e.printStackTrace()
attachmentNotification.uri, downloadState = AttachmentState.ERROR
attachment.binaryAttachment,
contentResolver, 1024) { percent ->
publishProgress(percent)
}
}
}
} }
} catch (e: Exception) {
Log.e(TAG, "Unable to upload or download file", e)
progressResult = false
} }
progressResult attachmentNotification.entryAttachmentState.downloadState
} }
// on post execute // on post execute
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val result = asyncResult.await() asyncAction.join()
attachmentNotification.attachmentFileAction = null
attachmentNotification.entryAttachmentState.apply { attachmentNotification.entryAttachmentState.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR if (downloadState != AttachmentState.CANCELED
downloadProgression = 100 && downloadState != AttachmentState.ERROR) {
downloadState = AttachmentState.COMPLETE
downloadProgression = 100
}
} }
attachmentNotification.attachmentFileAction = null
listener?.onUpdate(attachmentNotification) listener?.onUpdate(attachmentNotification)
TimeoutHelper.releaseTemporarilyDisableTimeout()
} }
} }
} }
fun cancel() {
attachmentNotification.entryAttachmentState.downloadState = AttachmentState.CANCELED
}
fun downloadFromDatabase(attachmentToUploadUri: Uri, fun downloadFromDatabase(attachmentToUploadUri: Uri,
binaryAttachment: BinaryAttachment, binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver, contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE, bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) { update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0L var dataDownloaded = 0L
val fileSize = binaryAttachment.length() val fileSize = binaryAttachment.length
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream -> UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
binaryAttachment.getUnGzipInputDataStream().use { inputStream -> Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
inputStream.readBytes(bufferSize) { buffer -> binaryAttachment.getUnGzipInputDataStream(binaryCipherKey).use { inputStream ->
outputStream.write(buffer) inputStream.readAllBytes(bufferSize) { buffer ->
dataDownloaded += buffer.size outputStream.write(buffer)
try { dataDownloaded += buffer.size
val percentDownload = (100 * dataDownloaded / fileSize).toInt() try {
update?.invoke(percentDownload) val percentDownload = (100 * dataDownloaded / fileSize).toInt()
} catch (e: Exception) { update?.invoke(percentDownload)
Log.e(TAG, "", e) } catch (e: Exception) {
Log.e(TAG, "", e)
}
} }
} }
} }
@@ -393,13 +425,14 @@ class AttachmentFileNotificationService: LockNotificationService() {
binaryAttachment: BinaryAttachment, binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver, contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE, bufferSize: Int = DEFAULT_BUFFER_SIZE,
canceled: ()-> Boolean = { false },
update: ((percent: Int)->Unit)? = null) { update: ((percent: Int)->Unit)? = null) {
var dataUploaded = 0L var dataUploaded = 0L
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0 val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream -> UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream ->
binaryAttachment.getGzipOutputDataStream().use { outputStream -> Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
BufferedInputStream(inputStream).use { attachmentBufferedInputStream -> binaryAttachment.getGzipOutputDataStream(binaryCipherKey).use { outputStream ->
attachmentBufferedInputStream.readBytes(bufferSize) { buffer -> inputStream.readAllBytes(bufferSize, canceled) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
dataUploaded += buffer.size dataUploaded += buffer.size
try { try {
@@ -419,7 +452,9 @@ class AttachmentFileNotificationService: LockNotificationService() {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) { if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachmentState.apply { attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS if (downloadState != AttachmentState.CANCELED) {
downloadState = AttachmentState.IN_PROGRESS
}
downloadProgression = percent downloadProgression = percent
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@@ -441,6 +476,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
private const val CHANNEL_ATTACHMENT_ID = "com.kunzisoft.keepass.notification.channel.attachment" private const val CHANNEL_ATTACHMENT_ID = "com.kunzisoft.keepass.notification.channel.attachment"
const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD" const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
const val ACTION_ATTACHMENT_FILE_STOP_UPLOAD = "ACTION_ATTACHMENT_FILE_STOP_UPLOAD"
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD" const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE" const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"

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.notifications package com.kunzisoft.keepass.services
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable

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.notifications package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context

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.notifications package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
@@ -39,6 +39,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
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.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -129,41 +130,50 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
fun checkDatabaseInfo() { fun checkDatabaseInfo() {
mDatabase.fileUri?.let { try {
val previousDatabaseInfo = mSnapFileDatabaseInfo mDatabase.fileUri?.let {
val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( val previousDatabaseInfo = mSnapFileDatabaseInfo
FileDatabaseInfo(applicationContext, it)) val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
val oldDatabaseModification = previousDatabaseInfo?.lastModification val oldDatabaseModification = previousDatabaseInfo?.lastModification
val newDatabaseModification = lastFileDatabaseInfo.lastModification val newDatabaseModification = lastFileDatabaseInfo.lastModification
val conditionExists = previousDatabaseInfo != null val conditionExists = previousDatabaseInfo != null
&& previousDatabaseInfo.exists != lastFileDatabaseInfo.exists && previousDatabaseInfo.exists != lastFileDatabaseInfo.exists
// To prevent dialog opening too often // To prevent dialog opening too often
val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null // Add 10 seconds delta time to prevent spamming
&& oldDatabaseModification < newDatabaseModification val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null
&& mLastLocalSaveTime + 5000 < newDatabaseModification) && oldDatabaseModification < newDatabaseModification
&& mLastLocalSaveTime + 10000 < newDatabaseModification)
if (conditionExists || conditionLastModification) { if (conditionExists || conditionLastModification) {
// Show the dialog only if it's real new info and not a delay after a save // Show the dialog only if it's real new info and not a delay after a save
Log.i(TAG, "Database file modified " + Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ") "$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info // Call listener to indicate a change in database info
if (previousDatabaseInfo != null) { if (previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener -> mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo) listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
}
} }
mSnapFileDatabaseInfo = lastFileDatabaseInfo
} }
mSnapFileDatabaseInfo = lastFileDatabaseInfo
} }
} catch (e: Exception) {
Log.e(TAG, "Unable to check database info", e)
} }
} }
fun saveDatabaseInfo() { fun saveDatabaseInfo() {
mDatabase.fileUri?.let { try {
mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( mDatabase.fileUri?.let {
FileDatabaseInfo(applicationContext, it)) mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo") FileDatabaseInfo(applicationContext, it))
Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check database info", e)
} }
} }
@@ -391,10 +401,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
intent?.removeExtra(DATABASE_TASK_WARNING_KEY) intent?.removeExtra(DATABASE_TASK_WARNING_KEY)
intent?.removeExtra(DATABASE_URI_KEY) intent?.removeExtra(DATABASE_URI_KEY)
intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY) intent?.removeExtra(MAIN_CREDENTIAL_KEY)
intent?.removeExtra(MASTER_PASSWORD_KEY)
intent?.removeExtra(KEY_FILE_CHECKED_KEY)
intent?.removeExtra(KEY_FILE_URI_KEY)
intent?.removeExtra(READ_ONLY_KEY) intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY) intent?.removeExtra(CIPHER_ENTITY_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
@@ -466,13 +473,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? { private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY) if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
) { ) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY) val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
if (databaseUri == null) if (databaseUri == null)
return null return null
@@ -482,14 +486,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseUri, databaseUri,
getString(R.string.database_default_name), getString(R.string.database_default_name),
getString(R.string.database), getString(R.string.database),
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), mainCredential
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
keyFileUri
) { result -> ) { result ->
result.data = Bundle().apply { result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(KEY_FILE_URI_KEY, keyFileUri) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
} }
} }
} else { } else {
@@ -500,15 +501,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? { private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY) if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
&& intent.hasExtra(READ_ONLY_KEY) && intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY) && intent.hasExtra(CIPHER_ENTITY_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) { ) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY) val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
@@ -519,8 +518,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
this, this,
mDatabase, mDatabase,
databaseUri, databaseUri,
masterPassword, mainCredential,
keyFileUri,
readOnly, readOnly,
cipherEntity, cipherEntity,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
@@ -529,8 +527,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Add each info to reload database after thrown duplicate UUID exception // Add each info to reload database after thrown duplicate UUID exception
result.data = Bundle().apply { result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(DATABASE_URI_KEY, databaseUri)
putString(MASTER_PASSWORD_KEY, masterPassword) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
putBoolean(READ_ONLY_KEY, readOnly) putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity) putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
} }
@@ -553,19 +550,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? { private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY) return if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
) { ) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this, AssignPasswordInDatabaseRunnable(this,
mDatabase, mDatabase,
databaseUri, databaseUri,
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
intent.getParcelableExtra(KEY_FILE_URI_KEY)
) )
} else { } else {
null null
@@ -888,10 +879,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY" const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val DATABASE_URI_KEY = "DATABASE_URI_KEY" const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY"
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"

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.notifications package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context

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.notifications package com.kunzisoft.keepass.services
import android.content.Intent import android.content.Intent
import com.kunzisoft.keepass.utils.LockReceiver import com.kunzisoft.keepass.utils.LockReceiver

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.notifications package com.kunzisoft.keepass.services
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager

View File

@@ -44,7 +44,7 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -386,7 +386,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
if (styleEnabled) { if (styleEnabled) {
Stylish.assignStyle(styleIdString) Stylish.assignStyle(styleIdString)
activity.recreate() // Relaunch the current activity to redraw theme
(activity as? SettingsActivity?)?.apply {
keepCurrentScreen()
startActivity(intent)
finish()
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
} }
styleEnabled styleEnabled
} }

View File

@@ -35,7 +35,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Database 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.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preference.*
import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.settings.preferencedialogfragment.*
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -58,7 +58,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null
private var mKeyDerivationPref: DialogListExplanationPreference? = null private var mKeyDerivationPref: DialogListExplanationPreference? = null
private var mRoundPref: InputKdfNumberPreference? = null private var mRoundPref: InputKdfNumberPreference? = null
private var mMemoryPref: InputKdfNumberPreference? = null private var mMemoryPref: InputKdfSizePreference? = null
private var mParallelismPref: InputKdfNumberPreference? = null private var mParallelismPref: InputKdfNumberPreference? = null
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) { override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
@@ -231,7 +231,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
} }
// Memory Usage // Memory Usage
mMemoryPref = findPreference<InputKdfNumberPreference>(getString(R.string.memory_usage_key))?.apply { mMemoryPref = findPreference<InputKdfSizePreference>(getString(R.string.memory_usage_key))?.apply {
summary = mDatabase.memoryUsage.toString() summary = mDatabase.memoryUsage.toString()
} }
@@ -553,7 +553,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
true true
} }
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseReload(false) settingActivity?.apply {
keepCurrentScreen()
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
}
return true return true
} }

View File

@@ -35,9 +35,13 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
} }
fun getScreen(): Screen {
return Screen.values()[requireArguments().getInt(TAG_KEY)]
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
onCreateScreenPreference( onCreateScreenPreference(
Screen.values()[requireArguments().getInt(TAG_KEY)], getScreen(),
savedInstanceState, savedInstanceState,
rootKey) rootKey)
} }

View File

@@ -36,9 +36,10 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionErrorIfNeeded
open class SettingsActivity open class SettingsActivity
: LockingActivity(), : LockingActivity(),
@@ -98,18 +99,23 @@ open class SettingsActivity
when (actionTask) { when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity // Reload the current activity
startActivity(intent) if (result.isSuccess) {
finish() startActivity(intent)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
} else {
this.showActionErrorIfNeeded(result)
finish()
}
} }
else -> { else -> {
// Call result in fragment // Call result in fragment
(supportFragmentManager (supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
?.onProgressDialogThreadResult(actionTask, result) ?.onProgressDialogThreadResult(actionTask, result)
coordinatorLayout?.showActionError(result)
} }
} }
coordinatorLayout?.showActionErrorIfNeeded(result)
} }
// To reload the current screen // To reload the current screen
@@ -117,6 +123,8 @@ open class SettingsActivity
intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName -> intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName ->
onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true) onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true)
} }
// Eat state
intent.removeExtra(FRAGMENT_ARG)
} }
} }
@@ -134,52 +142,33 @@ open class SettingsActivity
} }
override fun onPasswordEncodingValidateListener(databaseUri: Uri?, override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean, mainCredential: MainCredential) {
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
databaseUri?.let { databaseUri?.let {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword( mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri, databaseUri,
masterPasswordChecked, mainCredential
masterPassword,
keyFileChecked,
keyFile
) )
} }
} }
override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
Database.getInstance().let { database -> Database.getInstance().let { database ->
database.fileUri?.let { databaseUri -> database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation // Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) { if (database.validatePasswordEncoding(mainCredential)) {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword( mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri, databaseUri,
masterPasswordChecked, mainCredential
masterPassword,
keyFileChecked,
keyFile
) )
} else { } else {
PasswordEncodingDialogFragment.getInstance(databaseUri, PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
masterPasswordChecked, .show(supportFragmentManager, "passwordEncodingTag")
masterPassword,
keyFileChecked,
keyFile
).show(supportFragmentManager, "passwordEncodingTag")
} }
} }
} }
} }
override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {}
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) { private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
if (PreferencesUtil.showLockDatabaseButton(this)) { if (PreferencesUtil.showLockDatabaseButton(this)) {
@@ -224,11 +213,19 @@ open class SettingsActivity
} }
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key) toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
// To reload the current screen
intent.putExtra(FRAGMENT_ARG, key.name)
hideOrShowLockButton(key) hideOrShowLockButton(key)
} }
/**
* To keep the current screen when activity is reloaded
*/
fun keepCurrentScreen() {
(supportFragmentManager.findFragmentByTag(TAG_NESTED) as? NestedSettingsFragment?)
?.getScreen()?.let { fragmentKey ->
intent.putExtra(FRAGMENT_ARG, fragmentKey.name)
}
}
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) { override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) {
if (mTimeoutEnable) if (mTimeoutEnable)
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {

View File

@@ -25,7 +25,7 @@ import androidx.preference.DialogPreference
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
class InputKdfNumberPreference @JvmOverloads constructor(context: Context, open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)

View File

@@ -0,0 +1,53 @@
/*
* 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.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class InputKdfSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
if (summary == UNKNOWN_VALUE_STRING) {
super.setSummary("")
} else {
var summaryString = summary
try {
val memorySize = summary.toString().toLong()
summaryString = if (memorySize > 0) {
// To convert bytes to mebibytes
DataByte(memorySize, DataByte.ByteFormat.BYTE)
.toBetterByteFormat().toString(context)
} else {
memorySize.toString()
}
} catch (e: Exception) {
} finally {
super.setSummary(summaryString)
}
}
}
}

View File

@@ -36,7 +36,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context,
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence) {
if (summary == INFINITE_VALUE_STRING) { if (summary == INFINITE_VALUE_STRING) {
super.setSummary("") super.setSummary("")
} else { } else {
super.setSummary(summary) super.setSummary(summary)
} }

View File

@@ -0,0 +1,49 @@
/*
* 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.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
open class InputSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
var summaryString = summary
try {
val memorySize = summary.toString().toLong()
summaryString = if (memorySize >= 0) {
// To convert bytes to mebibytes
DataByte(memorySize, DataByte.ByteFormat.BYTE)
.toBetterByteFormat().toString(context)
} else {
memorySize.toString()
}
} catch (e: Exception) {
} finally {
super.setSummary(summaryString)
}
}
}

View File

@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
private var inputTextView: EditText? = null private var inputTextView: EditText? = null
private var textUnitView: TextView? = null
private var textExplanationView: TextView? = null private var textExplanationView: TextView? = null
private var switchElementView: CompoundButton? = null private var switchElementView: CompoundButton? = null
@@ -47,6 +48,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
}
fun setInputTextError(error: CharSequence) { fun setInputTextError(error: CharSequence) {
this.inputTextView?.error = error this.inputTextView?.error = error
} }
@@ -55,6 +64,24 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
this.mOnInputTextEditorActionListener = onEditorActionListener this.mOnInputTextEditorActionListener = onEditorActionListener
} }
var unitText: String?
get() = textUnitView?.text?.toString() ?: ""
set(unitText) {
textUnitView?.apply {
if (unitText != null && unitText.isNotEmpty()) {
text = unitText
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
}
fun setUnitText(@StringRes unitTextId: Int) {
unitText = getString(unitTextId)
}
var explanationText: String? var explanationText: String?
get() = textExplanationView?.text?.toString() ?: "" get() = textExplanationView?.text?.toString() ?: ""
set(explanationText) { set(explanationText) {
@@ -69,6 +96,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId)
}
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
@@ -93,6 +124,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
} }
textUnitView = view.findViewById(R.id.input_text_unit)
textUnitView?.visibility = View.GONE
textExplanationView = view.findViewById(R.id.explanation_text) textExplanationView = view.findViewById(R.id.explanation_text)
textExplanationView?.visibility = View.GONE textExplanationView?.visibility = View.GONE
switchElementView = view.findViewById(R.id.switch_element) switchElementView = view.findViewById(R.id.switch_element)
@@ -113,18 +146,6 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
return false return false
} }
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
}
fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId)
}
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) { fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
switchElementView?.isChecked = defaultChecked switchElementView?.isChecked = defaultChecked

View File

@@ -22,50 +22,76 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var dataByte = DataByte(2L, DataByte.ByteFormat.MEBIBYTE)
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
setExplanationText(R.string.max_history_size_summary) setExplanationText(R.string.max_history_size_summary)
database?.historyMaxSize?.let { maxItemsDatabase -> database?.historyMaxSize?.let { maxItemsDatabase ->
inputText = maxItemsDatabase.toString() dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
unitText = null
}
setSwitchAction({ isChecked -> setSwitchAction({ isChecked ->
inputText = if (!isChecked) { if (!isChecked) {
INFINITE_MAX_HISTORY_SIZE.toString() dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
} else inputText = INFINITE_MAX_HISTORY_SIZE.toString()
DEFAULT_MAX_HISTORY_SIZE.toString() unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked) showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE) }, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
} }
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) { if (positiveResult) {
database?.let { database -> database?.let { database ->
var maxHistorySize: Long = try { val maxHistorySize: Long = try {
inputText.toLong() inputText.toLong()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
DEFAULT_MAX_HISTORY_SIZE DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE.toBytes()
} }
if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) { val numberOfBytes = if (maxHistorySize >= 0) {
maxHistorySize = INFINITE_MAX_HISTORY_SIZE val dataByteConversion = DataByte(maxHistorySize, dataByte.format)
var bytes = dataByteConversion.toBytes()
if (bytes > Long.MAX_VALUE) {
bytes = Long.MAX_VALUE
}
bytes
} else {
INFINITE_MAX_HISTORY_SIZE
} }
val oldMaxHistorySize = database.historyMaxSize val oldMaxHistorySize = database.historyMaxSize
database.historyMaxSize = maxHistorySize database.historyMaxSize = numberOfBytes
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable) mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, numberOfBytes, mDatabaseAutoSaveEnable)
} }
} }
} }
companion object { companion object {
const val DEFAULT_MAX_HISTORY_SIZE = 134217728L
const val INFINITE_MAX_HISTORY_SIZE = -1L const val INFINITE_MAX_HISTORY_SIZE = -1L
private val INFINITE_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(INFINITE_MAX_HISTORY_SIZE, DataByte.ByteFormat.MEBIBYTE)
private val DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(6L, DataByte.ByteFormat.MEBIBYTE)
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat { fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
val fragment = MaxHistorySizePreferenceDialogFragmentCompat() val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
val bundle = Bundle(1) val bundle = Bundle(1)

View File

@@ -22,33 +22,46 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var dataByte = DataByte(MIN_MEMORY_USAGE, DataByte.ByteFormat.BYTE)
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
setExplanationText(R.string.memory_usage_explanation) setExplanationText(R.string.memory_usage_explanation)
inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
val memoryBytes = database?.memoryUsage ?: MIN_MEMORY_USAGE
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) { if (positiveResult) {
database?.let { database -> database?.let { database ->
var memoryUsage: Long = try { var newMemoryUsage: Long = try {
inputText.toLong() inputText.toLong()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
MIN_MEMORY_USAGE MIN_MEMORY_USAGE
} }
if (memoryUsage < MIN_MEMORY_USAGE) { if (newMemoryUsage < MIN_MEMORY_USAGE) {
memoryUsage = MIN_MEMORY_USAGE newMemoryUsage = MIN_MEMORY_USAGE
}
// To transform in bytes
dataByte.number = newMemoryUsage
var numberOfBytes = dataByte.toBytes()
if (numberOfBytes > Long.MAX_VALUE) {
numberOfBytes = Long.MAX_VALUE
} }
// TODO Max Memory
val oldMemoryUsage = database.memoryUsage val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage database.memoryUsage = numberOfBytes
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable) mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, numberOfBytes, mDatabaseAutoSaveEnable)
} }
} }
} }

View File

@@ -30,10 +30,12 @@ import java.util.*
* Read all data of stream and invoke [readBytes] each time the buffer is full or no more data to read. * Read all data of stream and invoke [readBytes] each time the buffer is full or no more data to read.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun InputStream.readBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) { fun InputStream.readAllBytes(bufferSize: Int = DEFAULT_BUFFER_SIZE,
cancelCondition: ()-> Boolean = { false },
readBytes: (bytesRead: ByteArray) -> Unit) {
val buffer = ByteArray(bufferSize) val buffer = ByteArray(bufferSize)
var read = 0 var read = 0
while (read != -1) { while (read != -1 && !cancelCondition()) {
read = this.read(buffer, 0, buffer.size) read = this.read(buffer, 0, buffer.size)
if (read != -1) { if (read != -1) {
val optimizedBuffer: ByteArray = if (buffer.size == read) { val optimizedBuffer: ByteArray = if (buffer.size == read) {
@@ -50,7 +52,8 @@ fun InputStream.readBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) ->
* Read number of bytes defined by [length] and invoke [readBytes] each time the buffer is full or no more data to read. * Read number of bytes defined by [length] and invoke [readBytes] each time the buffer is full or no more data to read.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun InputStream.readBytes(length: Int, bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) { fun InputStream.readBytes(length: Int, bufferSize: Int = DEFAULT_BUFFER_SIZE,
readBytes: (bytesRead: ByteArray) -> Unit) {
var bufferLength = bufferSize var bufferLength = bufferSize
var buffer = ByteArray(bufferLength) var buffer = ByteArray(bufferLength)

View File

@@ -31,10 +31,11 @@ import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
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.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_STOP_UPLOAD
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE
class AttachmentFileBinderManager(private val activity: FragmentActivity) { class AttachmentFileBinderManager(private val activity: FragmentActivity) {
@@ -120,6 +121,10 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
}, ACTION_ATTACHMENT_FILE_START_UPLOAD) }, ACTION_ATTACHMENT_FILE_START_UPLOAD)
} }
fun stopUploadAllAttachments() {
start(null, ACTION_ATTACHMENT_FILE_STOP_UPLOAD)
}
fun startDownloadAttachment(downloadFileUri: Uri, fun startDownloadAttachment(downloadFileUri: Uri,
attachment: Attachment) { attachment: Attachment) {
start(Bundle().apply { start(Bundle().apply {

View File

@@ -32,8 +32,8 @@ import android.util.Log
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.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper

View File

@@ -0,0 +1,84 @@
/*
* 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.utils
import android.content.Context
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
class DataByte(var number: Long, var format: ByteFormat) {
fun toBetterByteFormat(): DataByte {
return when (this.format) {
ByteFormat.BYTE -> {
when {
//this.number % GIBIBYTES == 0L -> {
// DataByte((this.number / GIBIBYTES), ByteFormat.GIBIBYTE)
//}
this.number % MEBIBYTES == 0L -> {
DataByte((this.number / MEBIBYTES), ByteFormat.MEBIBYTE)
}
this.number % KIBIBYTES == 0L -> {
DataByte((this.number / KIBIBYTES), ByteFormat.KIBIBYTE)
}
else -> {
DataByte(this.number, ByteFormat.BYTE)
}
}
}
else -> {
DataByte(toBytes(), ByteFormat.BYTE).toBetterByteFormat()
}
}
}
/**
* Number of bytes in current DataByte
*/
fun toBytes(): Long {
return when (this.format) {
ByteFormat.BYTE -> this.number
ByteFormat.KIBIBYTE -> this.number * KIBIBYTES
ByteFormat.MEBIBYTE -> this.number * MEBIBYTES
//ByteFormat.GIBIBYTE -> this.number * GIBIBYTES
}
}
override fun toString(): String {
return "$number ${format.name}"
}
fun toString(context: Context): String {
return "$number ${context.getString(format.stringId)}"
}
enum class ByteFormat(@StringRes var stringId: Int) {
BYTE(R.string.unit_byte),
KIBIBYTE(R.string.unit_kibibyte),
MEBIBYTE(R.string.unit_mebibyte)
//GIBIBYTE(R.string.unit_gibibyte)
}
companion object {
const val KIBIBYTES = 1024L
const val MEBIBYTES = 1048576L
const val GIBIBYTES = 1073741824L
}
}

View File

@@ -53,12 +53,12 @@ object StringDatabaseKDBUtils {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun writeStringToBytes(string: String?, os: OutputStream): Int { fun writeStringToStream(outputStream: OutputStream, string: String?): Int {
var str = string var str = string
if (str == null) { if (str == null) {
// Write out a null character // Write out a null character
os.write(uIntTo4Bytes(UnsignedInt(1))) outputStream.write(uIntTo4Bytes(UnsignedInt(1)))
os.write(0x00) outputStream.write(0x00)
return 0 return 0
} }
@@ -69,9 +69,9 @@ object StringDatabaseKDBUtils {
val initial = str.toByteArray(defaultCharset) val initial = str.toByteArray(defaultCharset)
val length = initial.size + 1 val length = initial.size + 1
os.write(uIntTo4Bytes(UnsignedInt(length))) outputStream.write(uIntTo4Bytes(UnsignedInt(length)))
os.write(initial) outputStream.write(initial)
os.write(0x00) outputStream.write(0x00)
return length return length
} }

View File

@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
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.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -321,6 +322,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
* ------------- * -------------
*/ */
fun setAttachmentCipherKey(cipherKey: Database.LoadedKey?) {
attachmentsAdapter.binaryCipherKey = cipherKey
}
private fun showAttachments(show: Boolean) { private fun showAttachments(show: Boolean) {
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
} }

View File

@@ -0,0 +1,107 @@
/*
* 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.view
import android.content.Context
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.text.util.LinkifyCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
class ExpirationView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: ConstraintLayout(context, attrs, defStyle) {
private var entryExpiresTextView: TextView
private var entryExpiresCheckBox: CompoundButton
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
private var fontInVisibility: Boolean = false
var setOnDateClickListener: (() -> Unit)? = null
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_expiration, this)
entryExpiresTextView = findViewById(R.id.expiration_text)
entryExpiresCheckBox = findViewById(R.id.expiration_checkbox)
entryExpiresTextView.setOnClickListener {
if (entryExpiresCheckBox.isChecked)
setOnDateClickListener?.invoke()
}
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(context)
}
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
expiresInstant.getDateTimeString(resources)
} else {
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
if (!value) {
expiresInstant = DateInstant.IN_ONE_MONTH
}
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiryTime: DateInstant
get() {
return if (expires)
expiresInstant
else
DateInstant.NEVER_EXPIRE
}
set(value) {
if (expires)
expiresInstant = value
assignExpiresDateText()
}
}

View File

@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.R
class ToolbarAction @JvmOverloads constructor(context: Context, class ToolbarAction @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = androidx.appcompat.R.attr.toolbarStyle) defStyle: Int = R.attr.actionToolbarAppearance)
: Toolbar(context, attrs, defStyle) { : Toolbar(context, attrs, defStyle) {
private var mActionModeCallback: ActionMode.Callback? = null private var mActionModeCallback: ActionMode.Callback? = null
@@ -39,7 +39,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
private var isOpen = false private var isOpen = false
init { init {
visibility = View.GONE setNavigationIcon(R.drawable.ic_close_white_24dp)
} }
fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode { fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode {
@@ -55,8 +55,6 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
actionMode.finish() actionMode.finish()
} }
setNavigationIcon(R.drawable.ic_close_white_24dp)
open() open()
return actionMode return actionMode

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.view
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorSet import android.animation.AnimatorSet
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Typeface import android.graphics.Typeface
@@ -35,6 +36,7 @@ import android.text.style.ClickableSpan
import android.view.View import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -166,7 +168,17 @@ fun View.updateLockPaddingLeft() {
)) ))
} }
fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) { fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->
Toast.makeText(this, errorId, Toast.LENGTH_LONG).show()
} ?: result.message?.let { message ->
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
}
}
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) { if (!result.isSuccess) {
result.exception?.errorId?.let { errorId -> result.exception?.errorId?.let { errorId ->
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show()

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -67,7 +67,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="@color/transparent" android:background="@color/transparent"
android:theme="?attr/toolbarAppearance" android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" app:popupTheme="?attr/toolbarPopupAppearance"
app:layout_collapseMode="pin" app:layout_collapseMode="pin"
tools:targetApi="lollipop"> tools:targetApi="lollipop">
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>

View File

@@ -33,27 +33,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<FrameLayout <com.kunzisoft.keepass.view.SpecialModeView
android:id="@+id/special_mode_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" > android:layout_height="?attr/actionBarSize"
<androidx.appcompat.widget.Toolbar android:theme="?attr/specialToolbarAppearance"
android:id="@+id/toolbar" app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title"
android:layout_width="match_parent" app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle"
android:layout_height="?attr/actionBarSize" app:layout_constraintTop_toTopOf="parent" />
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance"
tools:targetApi="lollipop" />
<com.kunzisoft.keepass.view.SpecialModeView
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="?attr/specialToolbarAppearance"
app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title"
app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@@ -79,24 +66,23 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<com.google.android.material.bottomappbar.BottomAppBar <com.kunzisoft.keepass.view.ToolbarAction
android:id="@+id/entry_edit_bottom_bar" android:id="@+id/entry_edit_bottom_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:fabAlignmentMode="center" app:popupTheme="?attr/toolbarPopupAppearance"
app:hideOnScroll="false"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_gravity="bottom" /> android:layout_gravity="bottom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entry_edit_validate" android:id="@+id/entry_edit_validate"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_anchorGravity="bottom|end" android:layout_gravity="center|bottom"
app:layout_anchor="@+id/entry_edit_bottom_bar" android:layout_marginBottom="10dp"
android:src="@drawable/ic_check_white_24dp" android:src="@drawable/ic_check_white_24dp"
android:contentDescription="@string/validate" android:contentDescription="@string/validate"
app:useCompatPadding="true" app:useCompatPadding="true"
app:fabSize="mini"
style="@style/KeepassDXStyle.Fab"/> style="@style/KeepassDXStyle.Fab"/>
<include <include

View File

@@ -130,7 +130,7 @@
android:elevation="4dp" android:elevation="4dp"
app:layout_collapseMode="pin" app:layout_collapseMode="pin"
android:theme="?attr/toolbarHomeAppearance" android:theme="?attr/toolbarHomeAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" /> app:popupTheme="?attr/toolbarPopupAppearance" />
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View File

@@ -64,7 +64,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance" android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" app:popupTheme="?attr/toolbarPopupAppearance"
android:elevation="4dp" android:elevation="4dp"
tools:targetApi="lollipop"> tools:targetApi="lollipop">
<LinearLayout <LinearLayout
@@ -155,9 +155,10 @@
android:id="@+id/toolbar_action" android:id="@+id/toolbar_action"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:visibility="gone"
app:popupTheme="?attr/toolbarPopupAppearance"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
style="?attr/actionToolbarAppearance" />
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/image_viewer_container"
android:background="?android:attr/windowBackground">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar_default"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<ProgressBar
android:id="@+id/image_viewer_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView
android:id="@+id/image_viewer_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:contentDescription="@string/entry_attachments" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -80,7 +80,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance" android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" app:popupTheme="?attr/toolbarPopupAppearance"
app:layout_collapseMode="pin" app:layout_collapseMode="pin"
tools:targetApi="lollipop"> tools:targetApi="lollipop">
<TextView <TextView
@@ -114,7 +114,12 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/default_margin" android:paddingTop="@dimen/default_margin"
android:paddingLeft="@dimen/default_margin"
android:paddingStart="@dimen/default_margin"
android:paddingRight="@dimen/default_margin"
android:paddingEnd="@dimen/default_margin"
android:paddingBottom="36dp"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -157,8 +162,8 @@
android:inputType="textPassword" android:inputType="textPassword"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:importantForAutofill="yes" android:importantForAutofill="yes"
android:autofillHints="password|" android:autofillHints="password"
android:imeOptions="actionDone" android:imeOptions="actionDone|flagNoPersonalizedLearning"
android:maxLines="1"/> android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</RelativeLayout> </RelativeLayout>

View File

@@ -106,6 +106,7 @@
android:inputType="textPassword|textMultiLine" android:inputType="textPassword|textMultiLine"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:importantForAutofill="no" android:importantForAutofill="no"
android:imeOptions="flagNoPersonalizedLearning"
android:ems="10" android:ems="10"
android:maxLines="10" android:maxLines="10"
android:hint="@string/entry_password"/> android:hint="@string/entry_password"/>
@@ -140,45 +141,11 @@
android:hint="@string/entry_url"/> android:hint="@string/entry_url"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<!-- Expires --> <!-- Expiration -->
<androidx.constraintlayout.widget.ConstraintLayout <com.kunzisoft.keepass.view.ExpirationView
android:id="@+id/entry_edit_expiration"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" > android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_edit_expires_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:text="@string/entry_expires"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_edit_expires_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
app:layout_constraintTop_toBottomOf="@+id/entry_edit_expires_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
style="@style/KeepassDXStyle.TextAppearance.Large"
tools:text="2020-03-04 05:00"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/entry_edit_expires_presets"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_edit_expires_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/entry_edit_expires_text"
app:layout_constraintEnd_toStartOf="@+id/entry_edit_expires_checkbox"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/entry_edit_expires_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_edit_expires_label"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Notes --> <!-- Notes -->
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout

View File

@@ -17,36 +17,59 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
--> -->
<LinearLayout <androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:padding="@dimen/default_margin" <LinearLayout
android:importantForAutofill="noExcludeDescendants" android:orientation="vertical"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/group_edit_icon_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/default_margin"
android:src="@drawable/ic_blank_32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/group_edit_name_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
<com.google.android.material.textfield.TextInputEditText android:padding="@dimen/default_margin"
android:id="@+id/group_edit_name" android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/group_edit_icon_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/default_margin"
android:src="@drawable/ic_blank_32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/group_edit_name_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/group_edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:inputType="text"
android:maxLines="1"
android:singleLine="true"
android:hint="@string/hint_group_name"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/group_edit_note_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/group_edit_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:inputType="textMultiLine"
android:maxLines="3"
android:hint="@string/entry_notes"/>
</com.google.android.material.textfield.TextInputLayout>
<com.kunzisoft.keepass.view.ExpirationView
android:id="@+id/group_edit_expiration"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="4dp" android:layout_marginLeft="4dp"
android:layout_marginStart="4dp" android:layout_marginStart="4dp" />
android:inputType="text" </LinearLayout>
android:maxLines="1" </androidx.core.widget.NestedScrollView>
android:singleLine="true"
android:hint="@string/hint_group_name"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -29,6 +29,18 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants" android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o"> tools:targetApi="o">
<TextView
android:id="@+id/setup_otp_type_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin"
android:layout_marginLeft="@dimen/card_view_margin"
android:layout_marginEnd="@dimen/card_view_margin"
android:layout_marginRight="@dimen/card_view_margin"
android:text="@string/error_otp_type"
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/card_view_otp_selection" android:id="@+id/card_view_otp_selection"
android:layout_margin="4dp" android:layout_margin="4dp"

View File

@@ -17,111 +17,138 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
--> -->
<androidx.constraintlayout.widget.ConstraintLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:targetApi="p" tools:targetApi="p"
android:id="@+id/item_attachment_container" android:id="@+id/item_attachment_container"
android:focusable="false" android:focusable="false"
android:orientation="horizontal" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_broken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/ic_attach_file_broken_white_24dp"
android:contentDescription="@string/entry_attachments" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_title" android:id="@+id/item_attachment_thumbnail"
app:layout_constraintTop_toTopOf="parent" android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="parent" android:layout_height="144dp"
app:layout_constraintStart_toEndOf="@+id/item_attachment_broken" android:layout_marginTop="12dp"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container" android:layout_centerHorizontal="true"
android:layout_width="0dp" android:visibility="gone"
android:background="?android:attr/windowBackground"
android:scaleType="fitStart" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_attachment_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" android:layout_alignBottom="@+id/item_attachment_thumbnail"
tools:text="BinaryFile.attach" /> android:background="?attr/cardBackgroundTransparentColor">
<LinearLayout
android:id="@+id/item_attachment_size_container" <androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content" android:id="@+id/item_attachment_broken"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:orientation="vertical" android:layout_height="wrap_content"
android:gravity="end" android:contentDescription="@string/entry_attachments"
app:layout_constraintTop_toTopOf="parent" android:src="@drawable/ic_attach_file_broken_white_24dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_action_container" > app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_size" android:id="@+id/item_attachment_title"
android:layout_width="wrap_content" style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:layout_height="wrap_content" android:layout_width="0dp"
android:firstBaselineToTopHeight="0dp"
android:includeFontPadding="false"
android:paddingStart="8dp"
android:paddingEnd="8dp"
tools:text="1.2 Mb" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_compression"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.Tiny"
android:firstBaselineToTopHeight="0dp"
android:includeFontPadding="false"
android:paddingStart="8dp"
android:paddingEnd="8dp"
tools:text="GZip" />
</LinearLayout>
<FrameLayout
android:id="@+id/item_attachment_action_container"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/item_attachment_delete_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintStart_toEndOf="@+id/item_attachment_broken"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" tools:text="BinaryFile.attach" />
android:layout_gravity="center"
android:contentDescription="@string/content_description_remove_field" <LinearLayout
android:focusable="true" android:id="@+id/item_attachment_size_container"
android:src="@drawable/ic_content_delete_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple" />
<FrameLayout
android:id="@+id/item_attachment_progress_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusable="false" android:gravity="end"
android:layout_gravity="center"> android:orientation="vertical"
<androidx.appcompat.widget.AppCompatImageView app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/item_attachment_icon" app:layout_constraintEnd_toStartOf="@+id/item_attachment_action_container"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:firstBaselineToTopHeight="0dp"
android:includeFontPadding="false"
tools:text="1.2 Mb" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_compression"
style="@style/KeepassDXStyle.TextAppearance.Tiny"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:firstBaselineToTopHeight="0dp"
android:includeFontPadding="false"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
tools:text="GZip" />
</LinearLayout>
<FrameLayout
android:id="@+id/item_attachment_action_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/item_attachment_delete_button"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_file_stream_white_24dp" android:contentDescription="@string/content_description_remove_field"
android:contentDescription="@string/download" android:focusable="true"
style="@style/KeepassDXStyle.ImageButton.Simple" /> android:src="@drawable/ic_content_delete_white_24dp"
<ProgressBar
android:id="@+id/item_attachment_progress"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/item_attachment_progress_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
style="@style/KeepassDXStyle.ProgressBar.Circle" android:focusable="false">
android:layout_width="36dp"
android:layout_height="36dp" <androidx.appcompat.widget.AppCompatImageView
android:max="100" android:id="@+id/item_attachment_icon"
android:progress="60" /> style="@style/KeepassDXStyle.ImageButton.Simple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/download"
android:src="@drawable/ic_file_stream_white_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/item_attachment_progress"
style="@style/KeepassDXStyle.ProgressBar.Circle"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:max="100"
android:progress="60"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
</FrameLayout> </FrameLayout>
</FrameLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </RelativeLayout>

View File

@@ -36,6 +36,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:imeOptions="flagNoPersonalizedLearning"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"

View File

@@ -46,14 +46,24 @@
app:layout_constraintTop_toBottomOf="@+id/explanation_text" app:layout_constraintTop_toBottomOf="@+id/explanation_text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:minHeight="48dp"/> android:minHeight="48dp"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/input_text" android:id="@+id/input_text"
android:layout_height="wrap_content"
android:layout_width="0dp" android:layout_width="0dp"
app:layout_constraintTop_toBottomOf="@+id/switch_element" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:minHeight="48dp"
android:digits="0123456789" android:digits="0123456789"
android:inputType="number"/> android:inputType="number"
android:minHeight="48dp"
app:layout_constraintEnd_toStartOf="@+id/input_text_unit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/switch_element" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_text_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/input_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/input_text" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -20,12 +20,13 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:title="@string/app_name" android:title="@string/app_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance" android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" app:popupTheme="?attr/toolbarPopupAppearance"
android:elevation="4dp" android:elevation="4dp"
tools:targetApi="lollipop" /> tools:targetApi="lollipop" />

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/expiration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:text="@string/entry_expires"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/expiration_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
app:layout_constraintTop_toBottomOf="@+id/expiration_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
style="@style/KeepassDXStyle.TextAppearance.Large"
tools:text="2020-03-04 05:00"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/expiration_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/expiration_label"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

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