mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ba058515 | ||
|
|
63fbca8029 | ||
|
|
b37966f79c | ||
|
|
bac5b0de5b | ||
|
|
5dac161553 | ||
|
|
528c167a88 | ||
|
|
cfe01aa996 | ||
|
|
0f53c975cc | ||
|
|
b7b99c77c8 | ||
|
|
1eea5412a5 | ||
|
|
4240465930 | ||
|
|
3dfbf7d2ad | ||
|
|
440490a4bb | ||
|
|
746382811b | ||
|
|
967a54dd50 | ||
|
|
289d9a2531 | ||
|
|
c56b4964fe | ||
|
|
79dbb942f9 | ||
|
|
a5bb5635d3 | ||
|
|
3efe43c0fe | ||
|
|
5fb5299d34 | ||
|
|
b988882251 | ||
|
|
85e82e3fb9 | ||
|
|
35cfe261d2 | ||
|
|
fe4faf9ebc | ||
|
|
751392d656 | ||
|
|
2e5ce5e94f | ||
|
|
535eeb2594 | ||
|
|
8f5e0e93ee | ||
|
|
6f17c5dcac | ||
|
|
4e7c7ba8ce | ||
|
|
6bd5b2345c | ||
|
|
63832ef8fd | ||
|
|
3719bf3593 | ||
|
|
7bca41ca72 | ||
|
|
3f6a9c3af5 | ||
|
|
309380bdd5 | ||
|
|
f135bdb905 | ||
|
|
2b926fd157 | ||
|
|
e911eea69c | ||
|
|
9d182b8299 | ||
|
|
61366e000f | ||
|
|
21cf49f4f8 | ||
|
|
30f0de83d3 | ||
|
|
42278a4b66 | ||
|
|
8752f92cea | ||
|
|
064c468e62 | ||
|
|
ef78fb749c | ||
|
|
a5d1db392b | ||
|
|
6234fc2ca3 | ||
|
|
c9cf90cdc9 | ||
|
|
5b033975b6 | ||
|
|
5663a153f7 | ||
|
|
4b73c45e65 | ||
|
|
ac248d8b73 | ||
|
|
726b0d0fa3 | ||
|
|
2d40164549 | ||
|
|
5203152f78 | ||
|
|
b064bb74cd | ||
|
|
843d8e8e77 | ||
|
|
c5f95b243d | ||
|
|
06f1f4c8ad | ||
|
|
9847f834c2 | ||
|
|
b744d58e6c | ||
|
|
c95358b344 | ||
|
|
2f15d6c9f2 | ||
|
|
d13aa047d5 | ||
|
|
7590d18c67 | ||
|
|
a689116c97 | ||
|
|
3bf7459f05 | ||
|
|
c70faaedd1 | ||
|
|
cb2417fbe4 | ||
|
|
65dd996f2e | ||
|
|
01790a6f31 | ||
|
|
8b84bb893d | ||
|
|
8cb99847c5 | ||
|
|
b3e01277d4 | ||
|
|
b10d407659 | ||
|
|
b7c5a5d238 | ||
|
|
90d1ce63e8 | ||
|
|
9e30b4e5f7 | ||
|
|
e541a8c629 | ||
|
|
0afe25c922 | ||
|
|
bca133430f | ||
|
|
9c925518a7 | ||
|
|
18e79b99e7 | ||
|
|
4e02846df9 | ||
|
|
2268b78bba | ||
|
|
ea8acd0677 | ||
|
|
9e931dd03f | ||
|
|
19e3aabca4 | ||
|
|
ae697d82d5 | ||
|
|
33382273c3 | ||
|
|
7d8466d77a | ||
|
|
5ca08a00d2 | ||
|
|
df3942697e | ||
|
|
5700ca5bcf | ||
|
|
54b2419d64 | ||
|
|
d8b1c94b78 | ||
|
|
2d1ffc23b9 | ||
|
|
e6b33d60c3 | ||
|
|
3fd06890d7 | ||
|
|
4af4ad7663 | ||
|
|
6ca8501e28 | ||
|
|
432b385f60 | ||
|
|
6cebdefa4a | ||
|
|
bc665eb83d | ||
|
|
cb187300fe | ||
|
|
da761614bd | ||
|
|
f34e007ecd | ||
|
|
3b6ad080b4 | ||
|
|
9919e90ba5 | ||
|
|
f4af44925b | ||
|
|
4bb366b568 | ||
|
|
7e7ab4ce19 | ||
|
|
4d833d25ce | ||
|
|
a9c508ecd9 | ||
|
|
ef4dbb8fdb | ||
|
|
9eb66face5 | ||
|
|
3fd13f3e3b | ||
|
|
319c9cad4b | ||
|
|
c12297c98d | ||
|
|
7c38361844 | ||
|
|
559554a975 | ||
|
|
7e2ffa2124 | ||
|
|
66dbac4bb2 | ||
|
|
8b6a843a85 | ||
|
|
976cff2751 | ||
|
|
f7c30fa8eb | ||
|
|
7757c8218b | ||
|
|
45da17adb8 | ||
|
|
58d10672ea |
18
CHANGELOG
18
CHANGELOG
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/androidTest/assets/test_image.png
Normal file
BIN
app/src/androidTest/assets/test_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
133
app/src/androidTest/assets/test_text.txt
Normal file
133
app/src/androidTest/assets/test_text.txt
Normal 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
|
||||||
|
℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟ ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ
|
||||||
|
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
|
||||||
|
! " # $ % & ' ( ) * + , - . / 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 { | } ~ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
|
||||||
|
Specials
|
||||||
|
|
||||||
|
Specials
|
||||||
|
<20>
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
1061
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
1061
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) { _, _ -> }
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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?) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.*
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.*
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
36
app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt
Normal file
36
app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt
Normal file
49
app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
84
app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt
Normal file
84
app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
107
app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt
Normal file
107
app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/keepassdx_logo.png
Normal file
BIN
app/src/main/res/drawable/keepassdx_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
32
app/src/main/res/layout/activity_image_viewer.xml
Normal file
32
app/src/main/res/layout/activity_image_viewer.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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" />
|
||||||
33
app/src/main/res/layout/view_expiration.xml
Normal file
33
app/src/main/res/layout/view_expiration.xml
Normal 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
Reference in New Issue
Block a user