diff --git a/CHANGELOG b/CHANGELOG
index 47f1b0fcb..854659cef 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,13 @@
+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
diff --git a/app/build.gradle b/app/build.gradle
index a9a280a45..34df6faf8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,12 +12,12 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 30
- versionCode = 56
- versionName = "2.9.12"
+ versionCode = 57
+ versionName = "2.9.13"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
- testInstrumentationRunner = "android.test.InstrumentationTestRunner"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
@@ -51,7 +51,7 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true"
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", "{}"
}
pro {
@@ -70,7 +70,7 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false"
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", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
}
@@ -82,6 +82,10 @@ android {
free.res.srcDir 'src/free/res'
}
+ testOptions {
+ unitTests.includeAndroidResources = true
+ }
+
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
@@ -120,14 +124,15 @@ dependencies {
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
// Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
- // Apache Commons Collections
+ // Apache Commons
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'
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')
// Tests
- androidTestImplementation 'junit:junit:4.13'
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test:rules:1.3.0'
}
diff --git a/app/src/androidTest/assets/test_image.png b/app/src/androidTest/assets/test_image.png
new file mode 100644
index 000000000..ec28a755c
Binary files /dev/null and b/app/src/androidTest/assets/test_image.png differ
diff --git a/app/src/androidTest/assets/test_text.txt b/app/src/androidTest/assets/test_text.txt
new file mode 100644
index 000000000..cc66555ac
--- /dev/null
+++ b/app/src/androidTest/assets/test_text.txt
@@ -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
+ �
+
diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt
new file mode 100644
index 000000000..b7bf2d8b4
--- /dev/null
+++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4cdd71c4b..7bcdcb9a3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -129,9 +129,12 @@
+
+ android:windowSoftInputMode="adjustResize" />
@@ -213,7 +216,7 @@
diff --git a/app/src/main/java/com/igreenwood/loupe/Loupe.kt b/app/src/main/java/com/igreenwood/loupe/Loupe.kt
new file mode 100644
index 000000000..e94a84411
--- /dev/null
+++ b/app/src/main/java/com/igreenwood/loupe/Loupe.kt
@@ -0,0 +1,1061 @@
+/*
+ * Loop 1.2.1 created by Issei Aoki modified by Jeremy JAMET
+ * https://github.com/igreenwood/loupe
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2020 Issei Aoki
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+package com.igreenwood.loupe
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.graphics.Matrix
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.BitmapDrawable
+import android.os.Build
+import android.util.TypedValue
+import android.view.*
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.widget.ImageView
+import android.widget.OverScroller
+import androidx.core.view.ViewCompat
+import java.lang.ref.WeakReference
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+
+class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
+ View.OnLayoutChangeListener {
+
+ companion object {
+ const val DEFAULT_MAX_ZOOM = 5.0f
+ const val DEFAULT_ANIM_DURATION = 250L
+ const val DEFAULT_ANIM_DURATION_LONG = 375L
+ const val DEFAULT_VIEW_DRAG_FRICTION = 1f
+ const val DEFAULT_DRAG_DISMISS_DISTANCE_IN_VIEW_HEIGHT_RATIO = 0.5f
+ const val DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP = 96
+ const val MAX_FLING_VELOCITY = 8000f
+ const val MIN_FLING_VELOCITY = 1500f
+ const val DEFAULT_DOUBLE_TAP_ZOOM_SCALE = 0.5f
+ val DEFAULT_INTERPOLATOR = DecelerateInterpolator()
+
+ fun create(
+ imageView: ImageView,
+ container: ViewGroup,
+ config: Loupe.() -> Unit = { }
+ ): Loupe {
+ return Loupe(imageView, container).apply(config)
+ }
+ }
+
+ interface OnViewTranslateListener {
+ fun onStart(view: ImageView)
+ fun onViewTranslate(view: ImageView, amount: Float)
+ fun onDismiss(view: ImageView)
+ fun onRestore(view: ImageView)
+ }
+
+ interface OnScaleChangedListener {
+ fun onScaleChange(scaleFactor: Float, focusX: Float, focusY: Float)
+ }
+
+ // max zoom(> 1f)
+ var maxZoom = DEFAULT_MAX_ZOOM
+ // use fling gesture for dismiss
+ var useFlingToDismissGesture = true
+ // flag to enable or disable drag to dismiss
+ var useDragToDismiss = true
+ // duration millis for dismiss animation
+ var dismissAnimationDuration = DEFAULT_ANIM_DURATION
+ // duration millis for restore animation
+ var restoreAnimationDuration = DEFAULT_ANIM_DURATION
+ // duration millis for image animation
+ var flingAnimationDuration = DEFAULT_ANIM_DURATION
+ // duration millis for double tap scale animation
+ var scaleAnimationDuration = DEFAULT_ANIM_DURATION_LONG
+ // duration millis for over scale animation
+ var overScaleAnimationDuration = DEFAULT_ANIM_DURATION_LONG
+ // duration millis for over scrolling animation
+ var overScrollAnimationDuration = DEFAULT_ANIM_DURATION
+ // view drag friction for swipe to dismiss(1f : drag distance == view move distance. Smaller value, view is moving more slower)
+ var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
+ // drag distance threshold in dp for swipe to dismiss
+ var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
+ // on view translate listener
+ var onViewTranslateListener: OnViewTranslateListener? = null
+ // on scale changed
+ var onScaleChangedListener: OnScaleChangedListener? = null
+
+ var dismissAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR
+
+ var restoreAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR
+
+ var flingAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR
+
+ var doubleTapScaleAnimationInterpolator: Interpolator = AccelerateDecelerateInterpolator()
+
+ var overScaleAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR
+
+ var overScrollAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR
+
+ var doubleTapZoomScale: Float = DEFAULT_DOUBLE_TAP_ZOOM_SCALE // 0f~1f
+
+ var minimumFlingVelocity: Float = MIN_FLING_VELOCITY
+
+ private var flingAnimator: Animator = ValueAnimator()
+
+ // bitmap matrix
+ private var transfrom = Matrix()
+ // bitmap scale
+ private var scale = 1f
+ // is ready for drawing bitmap
+ private var isReadyToDraw = false
+ // view rect - padding (recalculated on size changed)
+ private var canvasBounds = RectF()
+ // bitmap drawing rect (move on scroll, recalculated on scale changed)
+ private var bitmapBounds = RectF()
+ // displaying bitmap rect (does not move, recalculated on scale changed)
+ private var viewport = RectF()
+ // minimum scale of bitmap
+ private var minScale = 1f
+ // maximum scale of bitmap
+ private var maxScale = 1f
+ // bitmap (decoded) width
+ private var imageWidth = 0f
+ // bitmap (decoded) height
+ private var imageHeight = 0f
+
+ private val scroller: OverScroller
+ private var originalViewBounds = Rect()
+ private var dragToDismissThreshold = 0f
+ private var dragDismissDistanceInPx = 0f
+ private var isViewTranslateAnimationRunning = false
+ private var isVerticalScrollEnabled = true
+ private var isHorizontalScrollEnabled = true
+ private var isBitmapTranslateAnimationRunning = false
+ private var isBitmapScaleAnimationRunninng = false
+ private var initialY = 0f
+ // scaling helper
+ private var scaleGestureDetector: ScaleGestureDetector? = null
+ // translating helper
+ private var gestureDetector: GestureDetector? = null
+ private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
+ object : ScaleGestureDetector.OnScaleGestureListener {
+
+ override fun onScale(detector: ScaleGestureDetector?): Boolean {
+ if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
+ return false
+ }
+
+ val scaleFactor = detector?.scaleFactor ?: 1.0f
+ val focalX = detector?.focusX ?: bitmapBounds.centerX()
+ val focalY = detector?.focusY ?: bitmapBounds.centerY()
+
+ if (detector?.scaleFactor == 1.0f) {
+ // scale is not changing
+ return true
+ }
+
+ zoomToTargetScale(calcNewScale(scaleFactor), focalX, focalY)
+
+ return true
+ }
+
+ override fun onScaleBegin(p0: ScaleGestureDetector?): Boolean = true
+
+ override fun onScaleEnd(p0: ScaleGestureDetector?) {}
+ }
+
+ private val onGestureListener: GestureDetector.OnGestureListener =
+ object : GestureDetector.SimpleOnGestureListener() {
+ override fun onDown(e: MotionEvent?): Boolean = true
+
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent?,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ if (e2?.pointerCount != 1) {
+ return true
+ }
+
+ if (scale > minScale) {
+ processScroll(distanceX, distanceY)
+ } else if (useDragToDismiss && scale == minScale) {
+ processDrag(distanceY)
+ }
+ return true
+ }
+
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent?,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ e1 ?: return true
+
+ if (scale > minScale) {
+ processFlingBitmap(velocityX, velocityY)
+ } else {
+ processFlingToDismiss(velocityY)
+ }
+ return true
+ }
+
+ override fun onDoubleTap(e: MotionEvent?): Boolean {
+ e ?: return false
+
+ if (isBitmapScaleAnimationRunninng) {
+ return true
+ }
+
+ if (scale > minScale) {
+ zoomOutToMinimumScale()
+ } else {
+ zoomInToTargetScale(e)
+ }
+ return true
+ }
+ }
+
+ init {
+ container.apply {
+ background.alpha = 255
+ setOnTouchListener(this@Loupe)
+ addOnLayoutChangeListener(this@Loupe)
+ scaleGestureDetector = ScaleGestureDetector(context, onScaleGestureListener)
+ gestureDetector = GestureDetector(context, onGestureListener)
+ scroller = OverScroller(context)
+ dragDismissDistanceInPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dragDismissDistanceInDp.toFloat(),
+ resources.displayMetrics
+ )
+ }
+ imageView.apply {
+ imageMatrix = null
+ y = 0f
+ animate().cancel()
+ scaleType = ImageView.ScaleType.MATRIX
+ }
+ }
+
+ private var imageViewRef: WeakReference = WeakReference(imageView)
+ private var containerRef: WeakReference = WeakReference(container)
+
+ override fun onTouch(view: View?, event: MotionEvent?): Boolean {
+ event ?: return false
+ val imageView = imageViewRef.get() ?: return false
+ val container = containerRef.get() ?: return false
+
+ container.parent.requestDisallowInterceptTouchEvent(scale != minScale)
+
+ if (!imageView.isEnabled) {
+ return false
+ }
+
+ if (isViewTranslateAnimationRunning) {
+ return false
+ }
+
+ val scaleEvent = scaleGestureDetector?.onTouchEvent(event)
+ val isScaleAnimationIsRunning = scale < minScale
+ if (scaleEvent != scaleGestureDetector?.isInProgress && !isScaleAnimationIsRunning) {
+ // handle single touch gesture when scaling process is not running
+ gestureDetector?.onTouchEvent(event)
+ }
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ flingAnimator.cancel()
+ }
+ MotionEvent.ACTION_UP -> {
+ when {
+ scale == minScale -> {
+ if (!isViewTranslateAnimationRunning) {
+ dismissOrRestoreIfNeeded()
+ }
+ }
+ scale > minScale -> {
+ constrainBitmapBounds(true)
+ }
+ else -> {
+ zoomOutToMinimumScale(true)
+ }
+ }
+ }
+ }
+
+ setTransform()
+ imageView.postInvalidate()
+ return true
+ }
+
+ override fun onLayoutChange(
+ view: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ val imageView = imageViewRef.get() ?: return
+ val container = containerRef.get() ?: return
+
+ imageView.run {
+ setupLayout(left, top, right, bottom)
+ initialY = y
+ if (useFlingToDismissGesture) {
+ setDragToDismissDistance(DEFAULT_DRAG_DISMISS_DISTANCE_IN_VIEW_HEIGHT_RATIO)
+ } else {
+ setDragToDismissDistance(DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP)
+ }
+ container.background.alpha = 255
+ setTransform()
+ postInvalidate()
+ }
+ }
+
+ private fun startVerticalTranslateAnimation(velY: Float) {
+ val imageView = imageViewRef.get() ?: return
+
+ isViewTranslateAnimationRunning = true
+
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ imageView.run {
+ val translationY = if (velY > 0) {
+ originalViewBounds.top + height - top
+ } else {
+ originalViewBounds.top - height - top
+ }
+ animate()
+ .setDuration(dismissAnimationDuration)
+ .setInterpolator(dismissAnimationInterpolator)
+ .translationY(translationY.toFloat())
+ .setUpdateListener {
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(imageView, amount)
+ }
+ .setListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ onViewTranslateListener?.onDismiss(imageView)
+ cleanup()
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ }
+ } else {
+ ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
+ originalViewBounds.top + imageView.height - imageView.top
+ } else {
+ originalViewBounds.top - imageView.height - imageView.top
+ }.toFloat()).apply {
+ duration = dismissAnimationDuration
+ interpolator = dismissAnimationInterpolator
+ addUpdateListener {
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(imageView, amount)
+ }
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ // no op
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ onViewTranslateListener?.onDismiss(imageView)
+ cleanup()
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ start()
+ }
+ }
+ }
+
+ private fun processFlingBitmap(velocityX: Float, velocityY: Float) {
+ val imageView = imageViewRef.get() ?: return
+
+ var (velX, velY) = velocityX / scale to velocityY / scale
+
+ if (velX == 0f && velY == 0f) {
+ return
+ }
+
+ if (velX > MAX_FLING_VELOCITY) {
+ velX = MAX_FLING_VELOCITY
+ }
+
+ if (velY > MAX_FLING_VELOCITY) {
+ velY = MAX_FLING_VELOCITY
+ }
+
+ val (fromX, fromY) = bitmapBounds.left to bitmapBounds.top
+
+ scroller.forceFinished(true)
+ scroller.fling(
+ fromX.roundToInt(),
+ fromY.roundToInt(),
+ velX.roundToInt(),
+ velY.roundToInt(),
+ (viewport.right - bitmapBounds.width()).roundToInt(),
+ viewport.left.roundToInt(),
+ (viewport.bottom - bitmapBounds.height()).roundToInt(),
+ viewport.top.roundToInt()
+ )
+
+ ViewCompat.postInvalidateOnAnimation(imageView)
+
+ val toX = scroller.finalX.toFloat()
+ val toY = scroller.finalY.toFloat()
+
+ flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = flingAnimationDuration
+ interpolator = flingAnimationInterpolator
+ addUpdateListener {
+ val amount = it.animatedValue as Float
+ val newLeft = lerp(amount, fromX, toX)
+ val newTop = lerp(amount, fromY, toY)
+ bitmapBounds.offsetTo(newLeft, newTop)
+ ViewCompat.postInvalidateOnAnimation(imageView)
+ setTransform()
+ }
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ isBitmapTranslateAnimationRunning = true
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isBitmapTranslateAnimationRunning = false
+ constrainBitmapBounds()
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isBitmapTranslateAnimationRunning = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ }
+ flingAnimator.start()
+ }
+
+ private fun processScroll(distanceX: Float, distanceY: Float) {
+ val distX = if (isHorizontalScrollEnabled) {
+ -distanceX
+ } else {
+ 0f
+ }
+ val distY = if (isVerticalScrollEnabled) {
+ -distanceY
+ } else {
+ 0f
+ }
+ offsetBitmap(distX, distY)
+ setTransform()
+ }
+
+ private fun zoomInToTargetScale(e: MotionEvent) {
+ val imageView = imageViewRef.get() ?: return
+ val startScale = scale
+ val endScale = minScale * maxZoom * doubleTapZoomScale
+ val focalX = e.x
+ val focalY = e.y
+ ValueAnimator.ofFloat(startScale, endScale).apply {
+ duration = scaleAnimationDuration
+ interpolator = doubleTapScaleAnimationInterpolator
+ addUpdateListener {
+ zoomToTargetScale(it.animatedValue as Float, focalX, focalY)
+ ViewCompat.postInvalidateOnAnimation(imageView)
+ setTransform()
+ }
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ isBitmapScaleAnimationRunninng = true
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isBitmapScaleAnimationRunninng = false
+ if (endScale == minScale) {
+ zoomToTargetScale(minScale, focalX, focalY)
+ imageView.postInvalidate()
+ }
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isBitmapScaleAnimationRunninng = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ }.start()
+ }
+
+ private fun zoomOutToMinimumScale(isOverScaling: Boolean = false) {
+ val imageView = imageViewRef.get() ?: return
+ val startScale = scale
+ val endScale = minScale
+ val startLeft = bitmapBounds.left
+ val startTop = bitmapBounds.top
+ val endLeft = canvasBounds.centerX() - imageWidth * minScale * 0.5f
+ val endTop = canvasBounds.centerY() - imageHeight * minScale * 0.5f
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = if (isOverScaling) {
+ overScaleAnimationDuration
+ } else {
+ scaleAnimationDuration
+ }
+ interpolator = if (isOverScaling) {
+ overScaleAnimationInterpolator
+ } else {
+ doubleTapScaleAnimationInterpolator
+ }
+ addUpdateListener {
+ val value = it.animatedValue as Float
+ scale = lerp(value, startScale, endScale)
+ val newLeft = lerp(value, startLeft, endLeft)
+ val newTop = lerp(value, startTop, endTop)
+ calcBounds()
+ bitmapBounds.offsetTo(newLeft, newTop)
+ constrainBitmapBounds()
+ ViewCompat.postInvalidateOnAnimation(imageView)
+ setTransform()
+ }
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ isBitmapScaleAnimationRunninng = true
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isBitmapScaleAnimationRunninng = false
+ if (endScale == minScale) {
+ scale = minScale
+ calcBounds()
+ constrainBitmapBounds()
+ imageView.postInvalidate()
+ }
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isBitmapScaleAnimationRunninng = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ }.start()
+ }
+
+ private var lastDistY = Float.NaN
+
+ private fun processDrag(distanceY: Float) {
+ val imageView = imageViewRef.get() ?: return
+
+ if (lastDistY.isNaN()) {
+ lastDistY = distanceY
+ return
+ }
+
+ if (imageView.y == initialY) {
+ onViewTranslateListener?.onStart(imageView)
+ }
+
+ imageView.y -= distanceY * viewDragFriction // if viewDragRatio is 1.0f, view translation speed is equal to user scrolling speed.
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(imageView, amount)
+ }
+
+ private fun dismissOrRestoreIfNeeded() {
+ if (!isDragging() || isViewTranslateAnimationRunning) {
+ return
+ }
+ dismissOrRestore()
+ }
+
+ private fun dismissOrRestore() {
+ val imageView = imageViewRef.get() ?: return
+
+ if (shouldTriggerDragToDismissAnimation()) {
+ if (useFlingToDismissGesture) {
+ startDragToDismissAnimation()
+ } else {
+ onViewTranslateListener?.onDismiss(imageView)
+ cleanup()
+ }
+ } else {
+ restoreViewTransform()
+ }
+ }
+
+ private fun shouldTriggerDragToDismissAnimation() = dragDistance() > dragToDismissThreshold
+
+ private fun restoreViewTransform() {
+ val imageView = imageViewRef.get() ?: return
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ imageView.run {
+ animate()
+ .setDuration(restoreAnimationDuration)
+ .setInterpolator(restoreAnimationInterpolator)
+ .translationY((originalViewBounds.top - top).toFloat())
+ .setUpdateListener {
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(this, amount)
+ }
+ .setListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ // no op
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ onViewTranslateListener?.onRestore(imageView)
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ // no op
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ }
+ } else {
+ ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
+ duration = restoreAnimationDuration
+ interpolator = restoreAnimationInterpolator
+ addUpdateListener {
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(imageView, amount)
+ }
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ // no op
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ onViewTranslateListener?.onRestore(imageView)
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ // no op
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ start()
+ }
+ }
+ }
+
+ private fun startDragToDismissAnimation() {
+ val imageView = imageViewRef.get() ?: return
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ imageView.run {
+ val translationY = if (y - initialY > 0) {
+ originalViewBounds.top + height - top
+ } else {
+ originalViewBounds.top - height - top
+ }
+ animate()
+ .setDuration(dismissAnimationDuration)
+ .setInterpolator(AccelerateDecelerateInterpolator())
+ .translationY(translationY.toFloat())
+ .setUpdateListener {
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(this, amount)
+ }
+ .setListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ isViewTranslateAnimationRunning = true
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ onViewTranslateListener?.onDismiss(imageView)
+ cleanup()
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ }
+ } else {
+ ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply {
+ duration = dismissAnimationDuration
+ interpolator = AccelerateDecelerateInterpolator()
+ addUpdateListener {
+ val amount = calcTranslationAmount()
+ changeBackgroundAlpha(amount)
+ onViewTranslateListener?.onViewTranslate(imageView, amount)
+ }
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(p0: Animator?) {
+ isViewTranslateAnimationRunning = true
+ }
+
+ override fun onAnimationEnd(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ onViewTranslateListener?.onDismiss(imageView)
+ cleanup()
+ }
+
+ override fun onAnimationCancel(p0: Animator?) {
+ isViewTranslateAnimationRunning = false
+ }
+
+ override fun onAnimationRepeat(p0: Animator?) {
+ // no op
+ }
+ })
+ start()
+ }
+ }
+ }
+
+ private fun processFlingToDismiss(velocityY: Float) {
+ if (useFlingToDismissGesture && !isViewTranslateAnimationRunning) {
+ if (abs(velocityY) < minimumFlingVelocity) {
+ return
+ }
+ startVerticalTranslateAnimation(velocityY)
+ }
+ }
+
+ private fun calcTranslationAmount() =
+ constrain(
+ 0f,
+ norm(dragDistance(), 0f, originalViewBounds.height().toFloat()),
+ 1f
+ )
+
+ private fun dragDistance() = abs(viewOffsetY())
+
+ private fun isDragging() = dragDistance() > 0f
+
+ private fun viewOffsetY() = imageViewRef.get()?.y ?: 0 - initialY
+
+ /**
+ * targetScale: new scale
+ * focalX: focal x in current bitmapBounds
+ * focalY: focal y in current bitmapBounds
+ */
+ private fun zoomToTargetScale(targetScale: Float, focalX: Float, focalY: Float) {
+ scale = targetScale
+ val lastBounds = RectF(bitmapBounds)
+ // scale has changed, recalculate bitmap bounds
+ calcBounds()
+ // offset to focalPoint
+ offsetToZoomFocalPoint(focalX, focalY, lastBounds, bitmapBounds)
+ onScaleChangedListener?.onScaleChange(targetScale, focalX, focalY)
+ }
+
+ private fun setTransform() {
+ val imageView = imageViewRef.get() ?: return
+ transfrom.apply {
+ reset()
+ postTranslate(-imageWidth / 2, -imageHeight / 2)
+ postScale(scale, scale)
+ postTranslate(bitmapBounds.centerX(), bitmapBounds.centerY())
+ }
+ imageView.imageMatrix = transfrom
+ }
+
+ /**
+ * setup layout
+ */
+ private fun setupLayout(left: Int, top: Int, right: Int, bottom: Int) {
+ val imageView = imageViewRef.get() ?: return
+ originalViewBounds.set(left, top, right, bottom)
+ imageView.run {
+ val drawable = imageViewRef.get()?.drawable
+ val bitmap = (drawable as? BitmapDrawable)?.bitmap
+ if (width == 0 || height == 0 || drawable == null) return
+ imageWidth = (bitmap?.width ?: drawable.intrinsicWidth).toFloat()
+ imageHeight = (bitmap?.height ?: drawable.intrinsicHeight).toFloat()
+ val canvasWidth = (width - paddingLeft - paddingRight).toFloat()
+ val canvasHeight = (height - paddingTop - paddingBottom).toFloat()
+
+ calcScaleRange(canvasWidth, canvasHeight, imageWidth, imageHeight)
+ calcBounds()
+ constrainBitmapBounds()
+ isReadyToDraw = true
+ invalidate()
+ }
+ }
+
+ private fun constrainBitmapBounds(animate: Boolean = false) {
+ val imageView = imageViewRef.get() ?: return
+
+ if (isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
+ return
+ }
+
+ val offset = PointF()
+
+ // constrain viewport inside bitmap bounds
+ if (viewport.left < bitmapBounds.left) {
+ offset.x += viewport.left - bitmapBounds.left
+ }
+
+ if (viewport.top < bitmapBounds.top) {
+ offset.y += viewport.top - bitmapBounds.top
+ }
+
+ if (viewport.right > bitmapBounds.right) {
+ offset.x += viewport.right - bitmapBounds.right
+ }
+
+ if (viewport.bottom > bitmapBounds.bottom) {
+ offset.y += viewport.bottom - bitmapBounds.bottom
+ }
+
+ if (offset.equals(0f, 0f)) {
+ return
+ }
+
+ if (animate) {
+ if (!isVerticalScrollEnabled) {
+ bitmapBounds.offset(0f, offset.y)
+ offset.y = 0f
+ }
+
+ if (!isHorizontalScrollEnabled) {
+ bitmapBounds.offset(offset.x, 0f)
+ offset.x = 0f
+ }
+
+ val start = RectF(bitmapBounds)
+ val end = RectF(bitmapBounds).apply {
+ offset(offset.x, offset.y)
+ }
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = overScrollAnimationDuration
+ interpolator = overScrollAnimationInterpolator
+ addUpdateListener {
+ val amount = it.animatedValue as Float
+ val newLeft = lerp(amount, start.left, end.left)
+ val newTop = lerp(amount, start.top, end.top)
+ bitmapBounds.offsetTo(newLeft, newTop)
+ ViewCompat.postInvalidateOnAnimation(imageView)
+ setTransform()
+ }
+ }.start()
+ } else {
+ bitmapBounds.offset(offset.x, offset.y)
+ }
+ }
+
+ /**
+ * calc canvas/bitmap bounds
+ */
+ private fun calcBounds() {
+ val imageView = imageViewRef.get() ?: return
+
+ imageView.run {
+ // calc canvas bounds
+ canvasBounds = RectF(
+ paddingLeft.toFloat(),
+ paddingTop.toFloat(),
+ width - paddingRight.toFloat(),
+ height - paddingBottom.toFloat()
+ )
+ }
+ // calc bitmap bounds
+ bitmapBounds = RectF(
+ canvasBounds.centerX() - imageWidth * scale * 0.5f,
+ canvasBounds.centerY() - imageHeight * scale * 0.5f,
+ canvasBounds.centerX() + imageWidth * scale * 0.5f,
+ canvasBounds.centerY() + imageHeight * scale * 0.5f
+ )
+ // calc viewport
+ viewport = RectF(
+ max(canvasBounds.left, bitmapBounds.left),
+ max(canvasBounds.top, bitmapBounds.top),
+ min(canvasBounds.right, bitmapBounds.right),
+ min(canvasBounds.bottom, bitmapBounds.bottom)
+ )
+ // check scroll availability
+ isHorizontalScrollEnabled = true
+ isVerticalScrollEnabled = true
+
+ if (bitmapBounds.width() < canvasBounds.width()) {
+ isHorizontalScrollEnabled = false
+ }
+
+ if (bitmapBounds.height() < canvasBounds.height()) {
+ isVerticalScrollEnabled = false
+ }
+ }
+
+ private fun offsetBitmap(offsetX: Float, offsetY: Float) {
+ bitmapBounds.offset(offsetX, offsetY)
+ }
+
+ /**
+ * calc min/max scale and set initial scale
+ */
+ private fun calcScaleRange(
+ canvasWidth: Float,
+ canvasHeight: Float,
+ bitmapWidth: Float,
+ bitmapHeight: Float
+ ) {
+ val canvasRatio = canvasHeight / canvasWidth
+ val bitmapRatio = bitmapHeight / bitmapWidth
+ minScale = if (canvasRatio > bitmapRatio) {
+ canvasWidth / bitmapWidth
+ } else {
+ canvasHeight / bitmapHeight
+ }
+ scale = minScale
+ maxScale = minScale * maxZoom
+ }
+
+
+ private fun calcNewScale(newScale: Float): Float {
+ return min(maxScale, newScale * scale)
+ }
+
+ private fun constrain(min: Float, value: Float, max: Float): Float {
+ return max(min(value, max), min)
+ }
+
+ private fun offsetToZoomFocalPoint(
+ focalX: Float,
+ focalY: Float,
+ oldBounds: RectF,
+ newBounds: RectF
+ ) {
+ val oldX = constrain(viewport.left, focalX, viewport.right)
+ val oldY = constrain(viewport.top, focalY, viewport.bottom)
+ val newX = map(oldX, oldBounds.left, oldBounds.right, newBounds.left, newBounds.right)
+ val newY = map(oldY, oldBounds.top, oldBounds.bottom, newBounds.top, newBounds.bottom)
+ offsetBitmap(oldX - newX, oldY - newY)
+ }
+
+ private fun map(
+ value: Float,
+ srcStart: Float,
+ srcStop: Float,
+ dstStart: Float,
+ dstStop: Float
+ ): Float {
+ if (srcStop - srcStart == 0f) {
+ return 0f
+ }
+ return ((value - srcStart) * (dstStop - dstStart) / (srcStop - srcStart)) + dstStart
+ }
+
+ private fun lerp(amt: Float, start: Float, stop: Float): Float {
+ return start + (stop - start) * amt
+ }
+
+ private fun norm(value: Float, start: Float, stop: Float): Float {
+ return value / (stop - start)
+ }
+
+ private fun changeBackgroundAlpha(amount: Float) {
+ val container = containerRef.get() ?: return
+ val newAlpha = ((1.0f - amount) * 255).roundToInt()
+ container.background.mutate().alpha = newAlpha
+ }
+
+ fun setDragToDismissDistance(distance: Int) {
+ val imageView = imageViewRef.get() ?: return
+ dragToDismissThreshold = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ distance.toFloat(),
+ imageView.context.resources.displayMetrics
+ )
+ }
+
+ fun setDragToDismissDistance(heightRatio: Float) {
+ val imageView = imageViewRef.get() ?: return
+ dragToDismissThreshold = imageView.height * heightRatio
+ }
+
+ fun dismiss() {
+ // Animate down offscreen (the finish listener will call the cleanup method)
+ startVerticalTranslateAnimation(MIN_FLING_VELOCITY)
+ }
+
+ fun cleanup() {
+ containerRef.get()?.apply {
+ setOnTouchListener(null)
+ removeOnLayoutChangeListener(null)
+ }
+ imageViewRef.clear()
+ containerRef.clear()
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
index 7da549d96..526736668 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
@@ -51,22 +51,19 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryAttachmentState
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.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.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
-import com.kunzisoft.keepass.utils.MenuUtil
-import com.kunzisoft.keepass.utils.UriUtil
-import com.kunzisoft.keepass.utils.createDocument
-import com.kunzisoft.keepass.utils.onCreateDocumentResult
+import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.EntryContentsView
-import com.kunzisoft.keepass.view.showActionError
+import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import java.util.*
import kotlin.collections.HashMap
@@ -129,6 +126,7 @@ class EntryActivity : LockingActivity() {
historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
+ entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey)
entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button)
@@ -156,10 +154,11 @@ class EntryActivity : LockingActivity() {
}
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
+ this.showActionErrorIfNeeded(result)
finish()
}
}
- coordinatorLayout?.showActionError(result)
+ coordinatorLayout?.showActionErrorIfNeeded(result)
}
}
@@ -222,7 +221,9 @@ class EntryActivity : LockingActivity() {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
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
entryContentsView?.assignCreationDate(entryInfo.creationTime)
- entryContentsView?.assignModificationDate(entryInfo.modificationTime)
+ entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
// Manage history
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
index 5e3172466..146dccd21 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
@@ -36,7 +36,6 @@ import android.widget.DatePicker
import android.widget.TimePicker
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
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.education.EntryEditActivityEducation
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.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.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
-import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
+import com.kunzisoft.keepass.view.ToolbarAction
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 org.joda.time.DateTime
import java.util.*
@@ -99,7 +98,7 @@ class EntryEditActivity : LockingActivity(),
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var entryEditFragment: EntryEditFragment? = null
- private var entryEditAddToolBar: Toolbar? = null
+ private var entryEditAddToolBar: ToolbarAction? = null
private var validateButton: View? = null
private var lockView: View? = null
@@ -107,7 +106,7 @@ class EntryEditActivity : LockingActivity(),
private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false
- private var mTempAttachments = ArrayList()
+ private var mTempAttachments = ArrayList()
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -119,11 +118,12 @@ class EntryEditActivity : LockingActivity(),
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
- val toolbar = findViewById(R.id.toolbar)
- setSupportActionBar(toolbar)
- supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
+ // Bottom Bar
+ entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
+ setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
+ supportActionBar?.setDisplayShowTitleEnabled(false)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
@@ -198,14 +198,14 @@ class EntryEditActivity : LockingActivity(),
// Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) {
- entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
+ entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey)
}
supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
.commit()
entryEditFragment?.apply {
drawFactory = mDatabase?.drawFactory
- setOnDateClickListener = View.OnClickListener {
+ setOnDateClickListener = {
expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
@@ -236,51 +236,6 @@ class EntryEditActivity : LockingActivity(),
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
mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -338,10 +293,11 @@ class EntryEditActivity : LockingActivity(),
}
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
+ this.showActionErrorIfNeeded(result)
finish()
}
}
- coordinatorLayout?.showActionError(result)
+ coordinatorLayout?.showActionErrorIfNeeded(result)
}
}
@@ -398,29 +354,27 @@ class EntryEditActivity : LockingActivity(),
when (entryAttachmentState.downloadState) {
AttachmentState.START -> {
entryEditFragment?.apply {
- // When only one attachment is allowed
- if (!mAllowMultipleAttachments) {
- clearAttachments()
- }
putAttachment(entryAttachmentState)
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
- }
+ } // Add in temp list
+ mTempAttachments.add(entryAttachmentState)
}
AttachmentState.IN_PROGRESS -> {
entryEditFragment?.putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
- entryEditFragment?.apply {
- putAttachment(entryAttachmentState)
- // Scroll to the attachment position
- getAttachmentViewPosition(entryAttachmentState) {
+ entryEditFragment?.putAttachment(entryAttachmentState) {
+ entryEditFragment?.getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
+ AttachmentState.CANCELED -> {
+ entryEditFragment?.removeAttachment(entryAttachmentState)
+ }
AttachmentState.ERROR -> {
entryEditFragment?.removeAttachment(entryAttachmentState)
coordinatorLayout?.let {
@@ -516,10 +470,12 @@ class EntryEditActivity : LockingActivity(),
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
if (attachmentToUploadUri != null && attachment != null) {
+ // When only one attachment is allowed
+ if (!mAllowMultipleAttachments) {
+ entryEditFragment?.clearAttachments()
+ }
// Start uploading in service
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
*/
private fun saveEntry() {
+ mAttachmentFileBinderManager?.stopUploadAllAttachments()
// Get the temp entry
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
@@ -583,14 +540,34 @@ class EntryEditActivity : LockingActivity(),
Entry(mEntry!!)
}?.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
newEntry.setEntryInfo(mDatabase, newEntryInfo)
// Delete temp attachment if not used
- mTempAttachments.forEach {
+ mTempAttachments.forEach { tempAttachmentState ->
+ val tempAttachment = tempAttachmentState.attachment
mDatabase?.binaryPool?.let { binaryPool ->
- if (!newEntry.getAttachments(binaryPool).contains(it)) {
- mDatabase?.removeAttachmentIfNotUsed(it)
+ if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
+ mDatabase?.removeAttachmentIfNotUsed(tempAttachment)
}
}
}
@@ -619,12 +596,30 @@ class EntryEditActivity : LockingActivity(),
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
- MenuUtil.contributionMenuInflater(menuInflater, menu)
+ menuInflater.inflate(R.menu.entry_edit, menu)
return true
}
-
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 {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
}
@@ -676,8 +671,16 @@ class EntryEditActivity : LockingActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
- R.id.menu_contribute -> {
- MenuUtil.onContributionItemSelected(this)
+ R.id.menu_add_field -> {
+ addNewCustomField()
+ return true
+ }
+ R.id.menu_add_attachment -> {
+ addNewAttachment(item)
+ return true
+ }
+ R.id.menu_add_otp -> {
+ setupOTP()
return true
}
android.R.id.home -> {
@@ -787,6 +790,7 @@ class EntryEditActivity : LockingActivity(),
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
+ mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true
approved.invoke()
}.create().show()
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt
index acd3c5c37..98a975471 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt
@@ -26,10 +26,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
-import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
-import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.adapters.EntryAttachmentsItemsAdapter
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.icon.IconImage
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.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
+import com.kunzisoft.keepass.view.ExpirationView
import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
@@ -63,8 +63,7 @@ class EntryEditFragment: StylishFragment() {
private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View
- private lateinit var entryExpiresCheckBox: CompoundButton
- private lateinit var entryExpiresTextView: TextView
+ private lateinit var entryExpirationView: ExpirationView
private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
@@ -75,10 +74,9 @@ class EntryEditFragment: StylishFragment() {
private var fontInVisibility: Boolean = false
private var iconColor: Int = 0
- private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
var drawFactory: IconDrawableFactory? = null
- var setOnDateClickListener: View.OnClickListener? = null
+ var setOnDateClickListener: (() -> Unit)? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: View.OnClickListener? = null
var setOnEditCustomField: ((Field) -> Unit)? = null
@@ -86,6 +84,7 @@ class EntryEditFragment: StylishFragment() {
// Elements to modify the current entry
private var mEntryInfo = EntryInfo()
+ private var mBinaryCipherKey: Database.LoadedKey? = null
private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null
@@ -112,12 +111,8 @@ class EntryEditFragment: StylishFragment() {
entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it)
}
- entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
- entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
- entryExpiresTextView.setOnClickListener {
- if (entryExpiresCheckBox.isChecked)
- setOnDateClickListener?.onClick(it)
- }
+ entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
+ entryExpirationView.setOnDateClickListener = setOnDateClickListener
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
@@ -127,6 +122,7 @@ class EntryEditFragment: StylishFragment() {
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
+ attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey?
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
@@ -140,10 +136,6 @@ class EntryEditFragment: StylishFragment() {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
- entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
- assignExpiresDateText()
- }
-
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
@@ -178,7 +170,7 @@ class EntryEditFragment: StylishFragment() {
setOnEditCustomField = null
}
- fun getEntryInfo(): EntryInfo? {
+ fun getEntryInfo(): EntryInfo {
populateEntryWithViews()
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
get() {
- return entryExpiresCheckBox.isChecked
+ return entryExpirationView.expires
}
set(value) {
- if (!value) {
- expiresInstant = DateInstant.IN_ONE_MONTH
- }
- entryExpiresCheckBox.isChecked = value
- assignExpiresDateText()
+ entryExpirationView.expires = value
}
var expiryTime: DateInstant
get() {
- return if (expires)
- expiresInstant
- else
- DateInstant.NEVER_EXPIRE
+ return entryExpirationView.expiryTime
}
set(value) {
- if (expires)
- expiresInstant = value
- assignExpiresDateText()
+ entryExpirationView.expiryTime = value
}
var notes: String
@@ -502,9 +473,13 @@ class EntryEditFragment: StylishFragment() {
return attachmentsAdapter.contains(attachment)
}
- fun putAttachment(attachment: EntryAttachmentState) {
+ fun putAttachment(attachment: EntryAttachmentState,
+ onPreviewLoaded: (()-> Unit)? = null) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
+ attachmentsAdapter.onBinaryPreviewLoaded = {
+ onPreviewLoaded?.invoke()
+ }
}
fun removeAttachment(attachment: EntryAttachmentState) {
@@ -528,6 +503,7 @@ class EntryEditFragment: StylishFragment() {
override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
+ outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState)
@@ -535,12 +511,15 @@ class EntryEditFragment: StylishFragment() {
companion object {
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"
- fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
+ fun getInstance(entryInfo: EntryInfo?,
+ loadedKey: Database.LoadedKey?): EntryEditFragment {
return EntryEditFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
+ putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
index a4d313091..e2c4672c5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
@@ -52,12 +52,13 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
@@ -199,8 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
result.data?.getParcelable(DATABASE_URI_KEY)?.let { databaseUri ->
- val keyFileUri = result.data?.getParcelable(KEY_FILE_URI_KEY)
- databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
+ val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
+ databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
}
}
ACTION_DATABASE_LOAD_TASK -> {
@@ -330,9 +331,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
- override fun onAssignKeyDialogPositiveClick(
- masterPasswordChecked: Boolean, masterPassword: String?,
- keyFileChecked: Boolean, keyFile: Uri?) {
+ override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
try {
mDatabaseFileUri?.let { databaseUri ->
@@ -340,10 +339,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Create the new database
mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
- masterPasswordChecked,
- masterPassword,
- keyFileChecked,
- keyFile
+ mainCredential
)
}
} catch (e: Exception) {
@@ -353,11 +349,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
}
- override fun onAssignKeyDialogNegativeClick(
- masterPasswordChecked: Boolean, masterPassword: String?,
- keyFileChecked: Boolean, keyFile: Uri?) {
-
- }
+ override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
index 2ea93ef09..4a59aa394 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -19,7 +19,9 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
+import android.app.DatePickerDialog
import android.app.SearchManager
+import android.app.TimePickerDialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -33,9 +35,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import android.widget.Toast
+import android.widget.*
import androidx.annotation.RequiresApi
import androidx.appcompat.view.ActionMode
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.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
-import com.kunzisoft.keepass.database.element.Database
-import com.kunzisoft.keepass.database.element.Entry
-import com.kunzisoft.keepass.database.element.Group
-import com.kunzisoft.keepass.database.element.SortNodeEnum
-import com.kunzisoft.keepass.database.element.icon.IconImage
+import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
+import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_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.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.*
+import org.joda.time.DateTime
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
IconPickerDialogFragment.IconPickerListener,
+ DatePickerDialog.OnDateSetListener,
+ TimePickerDialog.OnTimeSetListener,
ListNodesFragment.NodeClickListener,
ListNodesFragment.NodesActionMenuListener,
DeleteNodesDialogFragment.DeleteNodeListener,
@@ -345,13 +345,18 @@ class GroupActivity : LockingActivity(),
}
ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
- startActivity(intent)
- finish()
- overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
+ if (result.isSuccess) {
+ startActivity(intent)
+ finish()
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
+ } else {
+ this.showActionErrorIfNeeded(result)
+ finish()
+ }
}
}
- coordinatorLayout?.showActionError(result)
+ coordinatorLayout?.showActionErrorIfNeeded(result)
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() {
actionNodeMode?.finish()
}
@@ -745,7 +783,7 @@ class GroupActivity : LockingActivity(),
when (node.type) {
Type.GROUP -> {
mOldGroupToUpdate = node as Group
- GroupEditDialogFragment.build(mOldGroupToUpdate!!)
+ GroupEditDialogFragment.build(mOldGroupToUpdate!!.getGroupInfo())
.show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP)
}
@@ -1031,19 +1069,17 @@ class GroupActivity : LockingActivity(),
}
}
- override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
- name: String?,
- icon: IconImage?) {
+ override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
+ groupInfo: GroupInfo) {
- if (name != null && name.isNotEmpty() && icon != null) {
+ if (groupInfo.title.isNotEmpty()) {
when (action) {
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
// If group creation
mCurrentGroup?.let { currentGroup ->
// Build the group
mDatabase?.createGroup()?.let { newGroup ->
- newGroup.title = name
- newGroup.icon = icon
+ newGroup.setGroupInfo(groupInfo)
// Not really needed here because added in runnable but safe
newGroup.parent = currentGroup
@@ -1063,9 +1099,7 @@ class GroupActivity : LockingActivity(),
// WARNING remove parent and children to keep memory
removeParent()
removeChildren()
-
- title = name
- this.icon = icon // TODO custom icon #96
+ this.setGroupInfo(groupInfo)
}
}
// If group updated save it in the database
@@ -1081,9 +1115,8 @@ class GroupActivity : LockingActivity(),
}
}
- override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
- name: String?,
- icon: IconImage?) {
+ override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
+ groupInfo: GroupInfo) {
// Do nothing here
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt
new file mode 100644
index 000000000..5d12da108
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt
@@ -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 .
+ *
+ */
+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(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(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)
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
index fd514a1a7..502a06581 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
@@ -56,14 +56,14 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
@@ -236,15 +236,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
- var masterPassword: String? = null
- var keyFileUri: Uri? = null
+ var mainCredential: MainCredential = MainCredential()
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
- masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
- keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
+ mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
@@ -252,8 +250,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
- masterPassword,
- keyFileUri,
+ mainCredential,
readOnly,
cipherEntity,
true)
@@ -534,8 +531,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(
databaseUri,
- password,
- keyFileUri,
+ MainCredential(password, keyFileUri),
readOnly,
cipherDatabaseEntity,
false)
@@ -544,15 +540,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
}
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
- password: String?,
- keyFile: Uri?,
+ mainCredential: MainCredential,
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
- password,
- keyFile,
+ mainCredential,
readOnly,
cipherDatabaseEntity,
fixDuplicateUUID
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt
index cabface80..857176e73 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt
@@ -37,6 +37,7 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
@@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
interface AssignPasswordDialogListener {
- fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
- keyFileChecked: Boolean, keyFile: Uri?)
- fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
- keyFileChecked: Boolean, keyFile: Uri?)
+ fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
+ fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
}
override fun onAttach(activity: Context) {
@@ -161,17 +160,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
if (!error) {
- mListener?.onAssignKeyDialogPositiveClick(
- passwordCheckBox!!.isChecked, mMasterPassword,
- keyFileCheckBox!!.isChecked, mKeyFile)
+ mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
- mListener?.onAssignKeyDialogNegativeClick(
- passwordCheckBox!!.isChecked, mMasterPassword,
- keyFileCheckBox!!.isChecked, mKeyFile)
+ mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
}
@@ -183,6 +178,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
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() {
super.onResume()
@@ -242,9 +243,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
- mListener?.onAssignKeyDialogPositiveClick(
- passwordCheckBox!!.isChecked, mMasterPassword,
- keyFileCheckBox!!.isChecked, mKeyFile)
+ mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss()
}
}
@@ -259,9 +258,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ ->
- mListener?.onAssignKeyDialogPositiveClick(
- passwordCheckBox!!.isChecked, mMasterPassword,
- keyFileCheckBox!!.isChecked, mKeyFile)
+ mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt
index d2cf2a1a5..b278bf815 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt
@@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
open class DeleteNodesDialogFragment : DialogFragment() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt
index bd5f945d3..542e8dcb9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
import com.kunzisoft.keepass.R
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() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
index a6b7a95c9..64082dd75 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
@@ -23,34 +23,39 @@ import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Bundle
-import com.google.android.material.textfield.TextInputLayout
-import androidx.fragment.app.DialogFragment
-import androidx.appcompat.app.AlertDialog
+import android.view.View
import android.widget.Button
import android.widget.ImageView
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.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database
-import com.kunzisoft.keepass.database.element.Group
-import com.kunzisoft.keepass.database.element.icon.IconImage
+import com.kunzisoft.keepass.database.element.DateInstant
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 {
private var mDatabase: Database? = null
- private var editGroupListener: EditGroupListener? = null
+ private var mEditGroupListener: EditGroupListener? = null
- private var editGroupDialogAction: EditGroupDialogAction? = null
- private var nameGroup: String? = null
- private var iconGroup: IconImage? = null
+ private var mEditGroupDialogAction = EditGroupDialogAction.NONE
+ private var mGroupInfo = GroupInfo()
- private var nameTextLayoutView: TextInputLayout? = null
- private var nameTextView: TextView? = null
- private var iconButtonView: ImageView? = null
+ private lateinit var iconButtonView: ImageView
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 {
CREATION, UPDATE, NONE;
@@ -67,7 +72,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
- editGroupListener = context as EditGroupListener
+ mEditGroupListener = context as EditGroupListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
@@ -76,16 +81,19 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
}
override fun onDetach() {
- editGroupListener = null
+ mEditGroupListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
- nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
- nameTextView = root?.findViewById(R.id.group_edit_name)
- iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
+ iconButtonView = root.findViewById(R.id.group_edit_icon_button)
+ nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
+ 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
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
@@ -94,46 +102,48 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
// Init elements
mDatabase = Database.getInstance()
- editGroupDialogAction = EditGroupDialogAction.NONE
- nameGroup = ""
- iconGroup = mDatabase?.iconFactory?.folderIcon
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ACTION_ID)
- && savedInstanceState.containsKey(KEY_NAME)
- && savedInstanceState.containsKey(KEY_ICON)) {
- editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
- nameGroup = savedInstanceState.getString(KEY_NAME)
- iconGroup = savedInstanceState.getParcelable(KEY_ICON)
-
+ && savedInstanceState.containsKey(KEY_GROUP_INFO)) {
+ mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
+ mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
} else {
arguments?.apply {
if (containsKey(KEY_ACTION_ID))
- editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
-
- if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
- nameGroup = getString(KEY_NAME)
- iconGroup = getParcelable(KEY_ICON)
+ mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
+ if (mEditGroupDialogAction === CREATION)
+ mGroupInfo.notes = ""
+ if (containsKey(KEY_GROUP_INFO)) {
+ mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
}
}
}
- // populate the name
- nameTextView?.text = nameGroup
- // populate the icon
- assignIconView()
+ // populate info in views
+ populateInfoToViews()
+ expirationView.setOnDateClickListener = {
+ 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)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ ->
- editGroupListener?.cancelEditGroup(
- editGroupDialogAction,
- nameTextView?.text?.toString(),
- iconGroup)
+ retrieveGroupInfoFromViews()
+ mEditGroupListener?.cancelEditGroup(
+ mEditGroupDialogAction,
+ mGroupInfo)
}
- iconButtonView?.setOnClickListener { _ ->
+ iconButtonView.setOnClickListener { _ ->
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
}
@@ -150,55 +160,87 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
if (d != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener {
+ retrieveGroupInfoFromViews()
if (isValid()) {
- editGroupListener?.approveEditGroup(
- editGroupDialogAction,
- nameTextView?.text?.toString(),
- iconGroup)
+ mEditGroupListener?.approveEditGroup(
+ mEditGroupDialogAction,
+ mGroupInfo)
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() {
- if (mDatabase?.drawFactory != null && iconGroup != null) {
- iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
+ if (mDatabase?.drawFactory != null) {
+ iconButtonView.assignDatabaseIcon(mDatabase?.drawFactory!!, mGroupInfo.icon, iconColor)
}
}
override fun iconPicked(bundle: Bundle) {
- iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
+ mGroupInfo.icon = IconPickerDialogFragment.getIconStandardFromBundle(bundle) ?: mGroupInfo.icon
assignIconView()
}
override fun onSaveInstanceState(outState: Bundle) {
- outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
- outState.putString(KEY_NAME, nameGroup)
- outState.putParcelable(KEY_ICON, iconGroup)
+ retrieveGroupInfoFromViews()
+ outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
+ outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState)
}
private fun isValid(): Boolean {
- if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
- nameTextLayoutView?.error = getString(R.string.error_no_name)
+ if (nameTextView.text.toString().isEmpty()) {
+ nameTextLayoutView.error = getString(R.string.error_no_name)
return false
}
return true
}
interface EditGroupListener {
- fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
- fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
+ fun approveEditGroup(action: EditGroupDialogAction,
+ groupInfo: GroupInfo)
+ fun cancelEditGroup(action: EditGroupDialogAction,
+ groupInfo: GroupInfo)
}
companion object {
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_GROUP_INFO = "KEY_GROUP_INFO"
fun build(): GroupEditDialogFragment {
val bundle = Bundle()
@@ -208,11 +250,10 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
return fragment
}
- fun build(group: Group): GroupEditDialogFragment {
+ fun build(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle()
- bundle.putString(KEY_NAME, group.title)
- bundle.putParcelable(KEY_ICON, group.icon)
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
+ bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupEditDialogFragment()
fragment.arguments = bundle
return fragment
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt
index 60da08681..e9f033e2d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt
@@ -26,6 +26,7 @@ import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.model.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() {
@@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
- val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
- 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)
+ val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
@@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onPasswordEncodingValidateListener(
databaseUri,
- masterPasswordChecked,
- masterPassword,
- keyFileChecked,
- keyFile
+ mainCredential
)
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
@@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() {
interface Listener {
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
- masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?)
+ mainCredential: MainCredential)
}
companion object {
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
- private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
- 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"
+ private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
fun getInstance(databaseUri: Uri,
- masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?): SortDialogFragment {
+ mainCredential: MainCredential): SortDialogFragment {
val fragment = SortDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
- putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
- putString(MASTER_PASSWORD_KEY, masterPassword)
- putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
- putParcelable(KEY_FILE_URI_KEY, keyFile)
+ putParcelable(MAIN_CREDENTIAL, mainCredential)
}
return fragment
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt
index 2315f9dbd..174904509 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt
@@ -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_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_dark) -> R.style.KeepassDXStyle_Purple_Dark
else -> R.style.KeepassDXStyle_Light
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt
index 0e276240e..011d4219c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt
@@ -120,7 +120,9 @@ abstract class AnimatedItemsAdapter- (val contex
}
fun clear() {
- itemsList.clear()
- notifyDataSetChanged()
+ if (itemsList.size > 0) {
+ itemsList.clear()
+ notifyDataSetChanged()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt
index 5564af29b..904aac8b6 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt
@@ -31,16 +31,22 @@ import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
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.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
+import com.kunzisoft.keepass.view.expand
class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter(context) {
+ var binaryCipherKey: Database.LoadedKey? = null
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
+ var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
private var mTitleColor: Int
@@ -62,6 +68,37 @@ class EntryAttachmentsItemsAdapter(context: Context)
val entryAttachmentState = itemsList[position]
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 {
setColorFilter(Color.RED)
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
@@ -77,7 +114,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
holder.binaryFileTitle.setTextColor(mTitleColor)
}
holder.binaryFileSize.text = Formatter.formatFileSize(context,
- entryAttachmentState.attachment.binaryAttachment.length())
+ entryAttachmentState.attachment.binaryAttachment.length)
holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources)
@@ -105,6 +142,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
}
AttachmentState.NULL,
AttachmentState.ERROR,
+ AttachmentState.CANCELED,
AttachmentState.COMPLETE -> {
holder.binaryFileProgressContainer.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 -> {
holder.binaryFileProgressIcon.isActivated = false
@@ -122,12 +160,17 @@ class EntryAttachmentsItemsAdapter(context: Context)
holder.binaryFileDeleteButton.visibility = View.GONE
holder.binaryFileProgress.apply {
visibility = when (entryAttachmentState.downloadState) {
- AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
- AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
+ AttachmentState.NULL,
+ AttachmentState.COMPLETE,
+ AttachmentState.CANCELED,
+ AttachmentState.ERROR -> View.GONE
+
+ AttachmentState.START,
+ AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachmentState.downloadProgression
}
- holder.itemView.setOnClickListener {
+ holder.binaryFileInfo.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState)
}
}
@@ -136,6 +179,8 @@ class EntryAttachmentsItemsAdapter(context: Context)
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 binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
index 44927d4e9..f68ba9ea7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
@@ -57,7 +57,7 @@ class NodeAdapter (private val context: Context)
private val mNodeSortedList: SortedList
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 mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F
diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt
index df871ebd5..eec183cda 100644
--- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt
@@ -25,7 +25,7 @@ import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
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.utils.SingletonHolderParameter
import java.util.*
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt
index 7c001c6a4..83b77a33b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
-import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
+import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
index c21307375..3bb1ed0f7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
@@ -24,39 +24,26 @@ import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
open class AssignPasswordInDatabaseRunnable (
context: Context,
database: Database,
protected val mDatabaseUri: Uri,
- withMasterPassword: Boolean,
- masterPassword: String?,
- withKeyFile: Boolean,
- keyFile: Uri?)
+ protected val mMainCredential: MainCredential)
: SaveDatabaseRunnable(context, database, true) {
- private var mMasterPassword: String? = null
- protected var mKeyFileUri: Uri? = null
-
private var mBackupKey: ByteArray? = null
- init {
- if (withMasterPassword)
- this.mMasterPassword = masterPassword
- if (withKeyFile)
- this.mKeyFileUri = keyFile
- }
-
override fun onStartRun() {
// Set key
try {
- // TODO move master key methods
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
- val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
- database.retrieveMasterKey(mMasterPassword, uriInputStream)
+ val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
+ database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) {
erase(mBackupKey)
setError(e)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt
index f2763f30a..0b55a1bb7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt
@@ -24,6 +24,7 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
@@ -32,12 +33,9 @@ class CreateDatabaseRunnable(context: Context,
databaseUri: Uri,
private val databaseName: String,
private val rootName: String,
- withMasterPassword: Boolean,
- masterPassword: String?,
- withKeyFile: Boolean,
- keyFile: Uri?,
+ mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?)
- : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
+ : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
override fun onStartRun() {
try {
@@ -61,7 +59,7 @@ class CreateDatabaseRunnable(context: Context,
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.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
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt
index f732f3779..a5c10b9bf 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt
@@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
-import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.UriUtil
class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mUri: Uri,
- private val mPass: String?,
- private val mKey: Uri?,
+ private val mMainCredential: MainCredential,
private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?,
private val mFixDuplicateUUID: Boolean,
@@ -51,10 +50,12 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onActionRun() {
try {
- mDatabase.loadData(mUri, mPass, mKey,
+ mDatabase.loadData(mUri,
+ mMainCredential,
mReadonly,
context.contentResolver,
UriUtil.getBinaryDir(context),
+ Database.LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID,
progressTaskUpdater)
}
@@ -67,7 +68,7 @@ class LoadDatabaseRunnable(private val context: Context,
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri,
- if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
+ if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
}
// Register the biometric
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt
index 534067403..2c68513f9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt
@@ -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.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
-import com.kunzisoft.keepass.notifications.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.notifications.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.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
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,
- masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?) {
+ mainCredential: MainCredential) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
- putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
- putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
- putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
- putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
+ putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_CREATE_TASK)
}
fun startDatabaseLoad(databaseUri: Uri,
- masterPassword: String?,
- keyFile: Uri?,
+ mainCredential: MainCredential,
readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?,
fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
- putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
- putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
+ putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -303,17 +296,11 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
}
fun startDatabaseAssignPassword(databaseUri: Uri,
- masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?) {
+ mainCredential: MainCredential) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
- putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
- putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
- putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
- putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
+ putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt
index 8951d3615..33e4c4def 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
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.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -34,7 +33,10 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
+ private var tempCipherKey: Database.LoadedKey? = null
+
override fun onStartRun() {
+ tempCipherKey = mDatabase.loadedCipherKey
// Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context))
}
@@ -43,9 +45,9 @@ class ReloadDatabaseRunnable(private val context: Context,
try {
mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context),
+ tempCipherKey ?: Database.LoadedKey.generateNewCipherKey(),
progressTaskUpdater)
- }
- catch (e: LoadDatabaseException) {
+ } catch (e: LoadDatabaseException) {
setError(e)
}
@@ -53,6 +55,7 @@ class ReloadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
+ tempCipherKey = null
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt
index 8b5ed3e7a..9c200d372 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt
@@ -19,9 +19,12 @@
*/
package com.kunzisoft.keepass.database.element
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
+import kotlinx.coroutines.*
data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable {
@@ -65,5 +68,28 @@ data class Attachment(var name: String,
override fun newArray(size: Int): Array {
return arrayOfNulls(size)
}
+
+ fun loadBitmap(attachment: Attachment,
+ binaryCipherKey: Database.LoadedKey?,
+ actionOnFinish: (Bitmap?) -> Unit) {
+ CoroutineScope(Dispatchers.Main).launch {
+ withContext(Dispatchers.IO) {
+ val asyncResult: Deferred = 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())
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
index 371358464..660ae0d16 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
@@ -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.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.stream.readBytes4ToUInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import java.io.*
+import java.security.Key
+import java.security.SecureRandom
import java.util.*
+import javax.crypto.KeyGenerator
+import javax.crypto.spec.IvParameterSpec
import kotlin.collections.ArrayList
@@ -74,6 +79,19 @@ class Database {
var loadTimestamp: Long? = null
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
get() {
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
@@ -320,12 +338,26 @@ class Database {
}
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
// Set Database state
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)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB,
@@ -366,16 +398,20 @@ class Database {
loaded = true
} catch (e: LoadDatabaseException) {
throw e
+ } catch (e: Exception) {
+ throw LoadDatabaseException(e)
} finally {
databaseInputStream?.close()
}
}
@Throws(LoadDatabaseException::class)
- fun loadData(uri: Uri, password: String?, keyfile: Uri?,
+ fun loadData(uri: Uri,
+ mainCredential: MainCredential,
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
+ tempCipherKey: LoadedKey,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@@ -389,8 +425,8 @@ class Database {
var keyFileInputStream: InputStream? = null
try {
// Get keyFile inputStream
- keyfile?.let {
- keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
+ mainCredential.keyFileUri?.let { keyFile ->
+ keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
// Read database stream for the first time
@@ -398,16 +434,18 @@ class Database {
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory)
.openDatabase(databaseInputStream,
- password,
+ mainCredential.masterPassword,
keyFileInputStream,
+ tempCipherKey,
progressTaskUpdater,
fixDuplicateUUID)
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory)
.openDatabase(databaseInputStream,
- password,
+ mainCredential.masterPassword,
keyFileInputStream,
+ tempCipherKey,
progressTaskUpdater,
fixDuplicateUUID)
}
@@ -427,27 +465,39 @@ class Database {
@Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File,
+ tempCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI
- fileUri?.let { oldDatabaseUri ->
- readDatabaseStream(contentResolver, oldDatabaseUri,
- { databaseInputStream ->
- DatabaseInputKDB(cacheDirectory)
- .openDatabase(databaseInputStream,
- masterKey,
- progressTaskUpdater)
- },
- { databaseInputStream ->
- DatabaseInputKDBX(cacheDirectory)
- .openDatabase(databaseInputStream,
- masterKey,
- progressTaskUpdater)
- }
- )
- } ?: run {
- Log.e(TAG, "Database URI is null, database cannot be reloaded")
- throw IODatabaseException()
+ try {
+ fileUri?.let { oldDatabaseUri ->
+ readDatabaseStream(contentResolver, oldDatabaseUri,
+ { databaseInputStream ->
+ DatabaseInputKDB(cacheDirectory)
+ .openDatabase(databaseInputStream,
+ masterKey,
+ tempCipherKey,
+ progressTaskUpdater)
+ },
+ { databaseInputStream ->
+ DatabaseInputKDBX(cacheDirectory)
+ .openDatabase(databaseInputStream,
+ masterKey,
+ tempCipherKey,
+ progressTaskUpdater)
+ }
+ )
+ } ?: 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)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt
index b985fbe38..56fcf7b17 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt
@@ -346,12 +346,6 @@ class Entry : Node, EntryVersionedInterface {
|| entryKDBX?.containsAttachment() == true
}
- private fun addAttachments(binaryPool: BinaryPool, attachments: List) {
- attachments.forEach {
- putAttachment(it, binaryPool)
- }
- }
-
private fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment)
@@ -427,7 +421,7 @@ class Entry : Node, EntryVersionedInterface {
entryInfo.username = username
entryInfo.password = password
entryInfo.creationTime = creationTime
- entryInfo.modificationTime = lastModificationTime
+ entryInfo.lastModificationTime = lastModificationTime
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url
@@ -467,7 +461,9 @@ class Entry : Node, EntryVersionedInterface {
notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool ->
- addAttachments(binaryPool, newEntryInfo.attachments)
+ newEntryInfo.attachments.forEach { attachment ->
+ putAttachment(attachment, binaryPool)
+ }
}
database?.stopManageEntry(this)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt
index 73ec4a35b..3478376db 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt
@@ -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.node.*
import com.kunzisoft.keepass.model.EntryInfo
+import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import kotlin.collections.ArrayList
@@ -232,6 +233,14 @@ class Group : Node, GroupVersionedInterface {
override val isCurrentlyExpires: Boolean
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
+ var notes: String?
+ get() = groupKDBX?.notes
+ set(value) {
+ value?.let {
+ groupKDBX?.notes = it
+ }
+ }
+
override fun getChildGroups(): List {
return groupKDB?.getChildGroups()?.map {
Group(it)
@@ -391,6 +400,35 @@ class Group : Node, GroupVersionedInterface {
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 {
if (this === other) return true
if (javaClass != other?.javaClass) return false
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt
index 623d64412..9d3d5fc45 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt
@@ -21,23 +21,33 @@ package com.kunzisoft.keepass.database.element.database
import android.os.Parcel
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.util.zip.GZIPInputStream
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 {
private var dataFile: File? = null
+ var length: Long = 0
+ private set
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
var isCorrupted: Boolean = false
-
- fun length(): Long {
- return dataFile?.length() ?: 0
- }
+ // Cipher to encrypt temp file
+ private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
+ private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
/**
* Empty protected binary
@@ -46,6 +56,7 @@ class BinaryAttachment : Parcelable {
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
this.dataFile = dataFile
+ this.length = 0
this.isCompressed = compressed
this.isProtected = protected
}
@@ -54,58 +65,77 @@ class BinaryAttachment : Parcelable {
parcel.readString()?.let {
dataFile = File(it)
}
+ length = parcel.readLong()
isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
}
@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 {
- 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))
}
}
@Throws(IOException::class)
- fun getUnGzipInputDataStream(): InputStream {
- return if (isCompressed)
- GZIPInputStream(getInputDataStream())
- else
- getInputDataStream()
- }
-
- @Throws(IOException::class)
- fun getOutputDataStream(): OutputStream {
+ private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
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")
}
}
@Throws(IOException::class)
- fun getGzipOutputDataStream(): OutputStream {
- return if (isCompressed) {
- GZIPOutputStream(getOutputDataStream())
- } else {
- getOutputDataStream()
- }
- }
-
- @Throws(IOException::class)
- fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
+ fun compress(cipherKey: Database.LoadedKey) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (!isCompressed) {
+ // Encrypt the new gzipped temp file
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
- GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
- getInputDataStream().use { inputStream ->
- inputStream.readBytes(bufferSize) { buffer ->
+ getInputDataStream(cipherKey).use { inputStream ->
+ GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
+ inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
- // Remove unGzip file
+ // Remove ungzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
@@ -117,13 +147,14 @@ class BinaryAttachment : Parcelable {
}
@Throws(IOException::class)
- fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
+ fun decompress(cipherKey: Database.LoadedKey) {
dataFile?.let { concreteDataFile ->
if (isCompressed) {
+ // Encrypt the new ungzipped temp file
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
- FileOutputStream(fileBinaryDecompress).use { outputStream ->
- getUnGzipInputDataStream().use { inputStream ->
- inputStream.readBytes(bufferSize) { buffer ->
+ getUnGzipInputDataStream(cipherKey).use { inputStream ->
+ buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
+ inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
@@ -170,6 +201,7 @@ class BinaryAttachment : Parcelable {
result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode()
+ result = 31 * result + length.hashCode()
return result
}
@@ -183,11 +215,31 @@ class BinaryAttachment : Parcelable {
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(dataFile?.absolutePath)
+ dest.writeLong(length)
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 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 {
private val TAG = BinaryAttachment::class.java.name
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt
index 8678e84ce..a72875753 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt
@@ -281,7 +281,5 @@ class DatabaseKDB : DatabaseVersioned() {
const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
-
- const val BUFFER_SIZE_BYTES = 3 * 128
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt
index ded08f2d4..d2ebabd20 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt
@@ -126,6 +126,7 @@ class DatabaseKDBX : DatabaseVersioned {
*/
constructor(databaseName: String, rootName: String) {
name = databaseName
+ kdbxVersion = FILE_VERSION_32_3
val group = createGroup().apply {
title = rootName
icon = iconFactory.folderIcon
@@ -212,8 +213,10 @@ class DatabaseKDBX : DatabaseVersioned {
private fun compressAllBinaries() {
binaryPool.doForEachBinary { binary ->
try {
+ val cipherKey = loadedCipherKey
+ ?: throw IOException("Unable to retrieve cipher key to compress binaries")
// To compress, create a new binary with file
- binary.compress(BUFFER_SIZE_BYTES)
+ binary.compress(cipherKey)
} catch (e: Exception) {
Log.e(TAG, "Unable to compress $binary", e)
}
@@ -223,7 +226,9 @@ class DatabaseKDBX : DatabaseVersioned {
private fun decompressAllBinaries() {
binaryPool.doForEachBinary { binary ->
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) {
Log.e(TAG, "Unable to decompress $binary", e)
}
@@ -708,7 +713,5 @@ class DatabaseKDBX : DatabaseVersioned {
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP
-
- const val BUFFER_SIZE_BYTES = 3 * 128
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt
index 660434e5f..560d8968e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt
@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.element.database
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.group.GroupVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
@@ -84,13 +85,13 @@ abstract class DatabaseVersioned<
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(IOException::class)
- fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
- masterKey = getMasterKey(key, keyInputStream)
+ fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
+ masterKey = getMasterKey(key, keyfileInputStream)
}
@Throws(IOException::class)
- protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
- val fileKey = getFileKey(keyInputStream)
+ protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
+ val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key)
val messageDigest: MessageDigest
@@ -383,6 +384,12 @@ abstract class DatabaseVersioned<
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 {
private const val TAG = "DatabaseVersioned"
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt
index e4e58e3f4..f6d877322 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt
@@ -316,7 +316,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte
var size = 0L
for ((label, poolId) in binaries) {
size += label.length.toLong()
- size += binaryPool[poolId]?.length() ?: 0
+ size += binaryPool[poolId]?.length ?: 0
}
return size
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt
index ad819cde5..147b7ce74 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt
@@ -19,6 +19,7 @@
*/
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.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -37,10 +38,12 @@ abstract class DatabaseInput>
*
* @throws LoadDatabaseException on database error (contains IO exceptions)
*/
+
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
- keyInputStream: InputStream?,
+ keyfileInputStream: InputStream?,
+ loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@@ -48,6 +51,7 @@ abstract class DatabaseInput>
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
+ loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt
index f2b67e788..c8664d5eb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.R
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.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -48,26 +49,30 @@ import javax.crypto.spec.SecretKeySpec
class DatabaseInputKDB(cacheDirectory: File)
: DatabaseInput(cacheDirectory) {
- private lateinit var mDatabaseToOpen: DatabaseKDB
+ private lateinit var mDatabase: DatabaseKDB
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
- keyInputStream: InputStream?,
+ keyfileInputStream: InputStream?,
+ loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
- mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
+ mDatabase.loadedCipherKey = loadedCipherKey
+ mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
+ loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
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)
- mDatabaseToOpen = DatabaseKDB()
+ mDatabase = DatabaseKDB()
- mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
+ mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke()
// Select algorithm
when {
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 -> {
- mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
+ mDatabase.encryptionAlgorithm = EncryptionAlgorithm.Twofish
}
else -> throw InvalidAlgorithmDatabaseException()
}
- mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
+ mDatabase.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong()
// Generate transformedMasterKey from masterKey
- mDatabaseToOpen.makeFinalKey(
+ mDatabase.makeFinalKey(
header.masterSeed,
header.transformSeed,
- mDatabaseToOpen.numberKeyEncryptionRounds)
+ mDatabase.numberKeyEncryptionRounds)
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
// Initialize Rijndael algorithm
val cipher: Cipher = try {
when {
- mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
+ mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
}
- mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
+ mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
}
else -> throw IOException("Encryption algorithm is not supported")
@@ -145,7 +150,7 @@ class DatabaseInputKDB(cacheDirectory: File)
try {
cipher.init(Cipher.DECRYPT_MODE,
- SecretKeySpec(mDatabaseToOpen.finalKey, "AES"),
+ SecretKeySpec(mDatabase.finalKey, "AES"),
IvParameterSpec(header.encryptionIV))
} catch (e1: InvalidKeyException) {
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())
- val newRoot = mDatabaseToOpen.createGroup()
+ val newRoot = mDatabase.createGroup()
newRoot.level = -1
- mDatabaseToOpen.rootGroup = newRoot
+ mDatabase.rootGroup = newRoot
// Import all nodes
var newGroup: GroupKDB? = null
@@ -192,12 +197,12 @@ class DatabaseInputKDB(cacheDirectory: File)
// Create new node depending on byte number
when (fieldSize) {
4 -> {
- newGroup = mDatabaseToOpen.createGroup().apply {
+ newGroup = mDatabase.createGroup().apply {
setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt())
}
}
16 -> {
- newEntry = mDatabaseToOpen.createEntry().apply {
+ newEntry = mDatabase.createEntry().apply {
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
}
}
@@ -211,7 +216,7 @@ class DatabaseInputKDB(cacheDirectory: File)
group.title = cipherInputStream.readBytesToString(fieldSize)
} ?:
newEntry?.let { entry ->
- val groupKDB = mDatabaseToOpen.createGroup()
+ val groupKDB = mDatabase.createGroup()
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt())
entry.parent = groupKDB
}
@@ -226,7 +231,7 @@ class DatabaseInputKDB(cacheDirectory: File)
if (iconId == -1) {
iconId = 0
}
- entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId)
+ entry.icon = mDatabase.iconFactory.getIcon(iconId)
}
}
0x0004 -> {
@@ -255,7 +260,7 @@ class DatabaseInputKDB(cacheDirectory: File)
}
0x0007 -> {
newGroup?.let { group ->
- group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
+ group.icon = mDatabase.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} ?:
newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
@@ -300,11 +305,12 @@ class DatabaseInputKDB(cacheDirectory: File)
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
- val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
+ val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory)
entry.binaryData = binaryAttachment
- BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
- cipherInputStream.readBytes(fieldSize,
- DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
+ val cipherKey = mDatabase.loadedCipherKey
+ ?: throw IOException("Unable to retrieve cipher key to load binaries")
+ BufferedOutputStream(binaryAttachment.getOutputDataStream(cipherKey)).use { outputStream ->
+ cipherInputStream.readBytes(fieldSize) { buffer ->
outputStream.write(buffer)
}
}
@@ -314,12 +320,12 @@ class DatabaseInputKDB(cacheDirectory: File)
0xFFFF -> {
// End record. Save node and count it.
newGroup?.let { group ->
- mDatabaseToOpen.addGroupIndex(group)
+ mDatabase.addGroupIndex(group)
currentGroupNumber++
newGroup = null
}
newEntry?.let { entry ->
- mDatabaseToOpen.addEntryIndex(entry)
+ mDatabase.addEntryIndex(entry)
currentEntryNumber++
newEntry = null
}
@@ -337,20 +343,20 @@ class DatabaseInputKDB(cacheDirectory: File)
constructTreeFromIndex()
} catch (e: LoadDatabaseException) {
- mDatabaseToOpen.clearCache()
+ mDatabase.clearCache()
throw e
} catch (e: IOException) {
- mDatabaseToOpen.clearCache()
+ mDatabase.clearCache()
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
- mDatabaseToOpen.clearCache()
+ mDatabase.clearCache()
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
- mDatabaseToOpen.clearCache()
+ mDatabase.clearCache()
throw LoadDatabaseException(e)
}
- return mDatabaseToOpen
+ return mDatabase
}
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator) {
@@ -375,18 +381,18 @@ class DatabaseInputKDB(cacheDirectory: File)
}
private fun constructTreeFromIndex() {
- mDatabaseToOpen.rootGroup?.let {
+ mDatabase.rootGroup?.let {
// add each group
- val groupIterator = mDatabaseToOpen.getGroupIndexes().iterator()
+ val groupIterator = mDatabase.getGroupIndexes().iterator()
if (groupIterator.hasNext())
buildTreeGroups(it, groupIterator.next(), groupIterator)
// add each child
- for (currentEntry in mDatabaseToOpen.getEntryIndexes()) {
+ for (currentEntry in mDatabase.getEntryIndexes()) {
if (currentEntry.parent != null) {
// 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)
currentEntry.parent = parentGroupRetrieve
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt
index 14276b2a9..185af9ba6 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt
@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine
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.DeletedObject
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
@@ -96,20 +97,24 @@ class DatabaseInputKDBX(cacheDirectory: File)
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
- keyInputStream: InputStream?,
+ keyfileInputStream: InputStream?,
+ loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
- mDatabase.retrieveMasterKey(password, keyInputStream)
+ mDatabase.loadedCipherKey = loadedCipherKey
+ mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
+ loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
+ mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
}
}
@@ -273,8 +278,10 @@ class DatabaseInputKDBX(cacheDirectory: File)
val byteLength = size - 1
// No compression at this level
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
- protectedBinary.getOutputDataStream().use { outputStream ->
- dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
+ val cipherKey = mDatabase.loadedCipherKey
+ ?: throw IOException("Unable to retrieve cipher key to load binaries")
+ protectedBinary.getOutputDataStream(cipherKey).use { outputStream ->
+ dataInputStream.readBytes(byteLength) { buffer ->
outputStream.write(buffer)
}
}
@@ -1009,14 +1016,16 @@ class DatabaseInputKDBX(cacheDirectory: File)
// Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
+ val binaryCipherKey = mDatabase.loadedCipherKey
+ ?: throw IOException("Unable to retrieve cipher key to load binaries")
try {
- binaryAttachment.getOutputDataStream().use { outputStream ->
+ binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
}
} catch (e: Exception) {
Log.e(TAG, "Unable to read base 64 attachment", e)
binaryAttachment.isCorrupted = true
- binaryAttachment.getOutputDataStream().use { outputStream ->
+ binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
outputStream.write(base64.toByteArray())
}
}
@@ -1083,6 +1092,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
return xpp
}
}
+
}
@Throws(IOException::class, XmlPullParserException::class)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt
index 417865c21..6c145529f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt
@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.crypto.CipherFactory
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.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
@@ -197,15 +197,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
@Suppress("CAST_NEVER_SUCCEEDS")
@Throws(DatabaseOutputException::class)
- fun outputPlanGroupAndEntries(os: OutputStream) {
- val los = LittleEndianDataOutputStream(os)
+ fun outputPlanGroupAndEntries(outputStream: OutputStream) {
+ val littleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
// useHeaderHash
if (headerHashBlock != null) {
try {
- los.writeUShort(0x0000)
- los.writeInt(headerHashBlock!!.size)
- los.write(headerHashBlock!!)
+ littleEndianDataOutputStream.writeUShort(0x0000)
+ littleEndianDataOutputStream.writeInt(headerHashBlock!!.size)
+ littleEndianDataOutputStream.write(headerHashBlock!!)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output header hash.", e)
}
@@ -213,20 +213,13 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// Groups
mDatabaseKDB.doForEachGroupInIndex { group ->
- val pgo = GroupOutputKDB(group, os)
- try {
- pgo.output()
- } catch (e: IOException) {
- throw DatabaseOutputException("Failed to output a tree", e)
- }
+ GroupOutputKDB(group, outputStream).output()
}
+ // Entries
+ val binaryCipherKey = mDatabaseKDB.loadedCipherKey
+ ?: throw DatabaseOutputException("Unable to retrieve cipher key to write binaries")
mDatabaseKDB.doForEachEntryInIndex { entry ->
- val peo = EntryOutputKDB(entry, os)
- try {
- peo.output()
- } catch (e: IOException) {
- throw DatabaseOutputException("Failed to output an entry.", e)
- }
+ EntryOutputKDB(entry, outputStream, binaryCipherKey).output()
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt
index 9fb0f0d37..27a98c388 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt
@@ -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.DatabaseKDBX
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.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
@@ -140,12 +139,14 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
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
- protectedBinary.decompress()
+ protectedBinary.decompress(binaryCipherKey)
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
- dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
+ dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
@@ -153,8 +154,8 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
dataOutputStream.writeByte(flag)
- protectedBinary.getInputDataStream().use { inputStream ->
- inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
+ protectedBinary.getInputDataStream(binaryCipherKey).use { inputStream ->
+ inputStream.readAllBytes { buffer ->
dataOutputStream.write(buffer)
}
}
@@ -473,7 +474,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
binary.getInputDataStream().use { inputStream ->
- inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
+ inputStream.readBytes { buffer ->
val encoded = ByteArray(buffer.size)
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
@@ -482,7 +483,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} else {
// Write the XML
binary.getInputDataStream().use { inputStream ->
- inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
+ inputStream.readBytes { buffer ->
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.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
val binary = keyBinary.binary
- if (binary.length() > 0) {
+ if (binary.length > 0) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Write the XML
- binary.getInputDataStream().use { inputStream ->
- inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
+ val binaryCipherKey = mDatabaseKDBX.loadedCipherKey
+ ?: 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)))
}
}
@@ -589,7 +592,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
}
} else {
- xml.text(safeXmlString(value.toString()))
+ xml.text(value.toString())
}
xml.endTag(null, DatabaseKDBXXML.ElemValue)
@@ -718,17 +721,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (text.isEmpty()) {
return text
}
-
val stringBuilder = StringBuilder()
- var ch: Char
+ var character: Char
for (element in text) {
- ch = element
+ character = element
+ val hexChar = character.toInt()
if (
- ch.toInt() in 0x20..0xD7FF ||
- ch.toInt() == 0x9 || ch.toInt() == 0xA || ch.toInt() == 0xD ||
- ch.toInt() in 0xE000..0xFFFD
+ hexChar in 0x20..0xD7FF ||
+ hexChar == 0x9 ||
+ hexChar == 0xA ||
+ hexChar == 0xD ||
+ hexChar in 0xE000..0xFFFD
) {
- stringBuilder.append(ch)
+ stringBuilder.append(character)
}
}
return stringBuilder.toString()
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt
index e06f6eb9d..25461e985 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt
@@ -19,9 +19,9 @@
*/
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.file.output.GroupOutputKDB.Companion.GROUPID_FIELD_SIZE
+import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
import com.kunzisoft.keepass.utils.UnsignedInt
@@ -29,96 +29,91 @@ import java.io.IOException
import java.io.OutputStream
import java.nio.charset.Charset
-class EntryOutputKDB
/**
* Output the GroupKDB to the stream
*/
-(private val mEntry: EntryKDB, private val mOutputStream: OutputStream) {
- /**
- * Returns the number of bytes written by the stream
- * @return Number of bytes written
- */
- var length: Long = 0
- private set
+class EntryOutputKDB(private val mEntry: EntryKDB,
+ private val mOutputStream: OutputStream,
+ private val mCipherKey: Database.LoadedKey) {
//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() {
+ 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
- mOutputStream.write(UUID_FIELD_TYPE)
- mOutputStream.write(UUID_FIELD_SIZE)
- mOutputStream.write(uuidTo16Bytes(mEntry.id))
+ // Image ID
+ mOutputStream.write(IMAGEID_FIELD_TYPE)
+ mOutputStream.write(IMAGEID_FIELD_SIZE)
+ mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId)))
- // Group ID
- mOutputStream.write(GROUPID_FIELD_TYPE)
- mOutputStream.write(GROUPID_FIELD_SIZE)
- mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id)))
+ // Title
+ //byte[] title = mEntry.title.getBytes("UTF-8");
+ mOutputStream.write(TITLE_FIELD_TYPE)
+ StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.title)
- // Image ID
- mOutputStream.write(IMAGEID_FIELD_TYPE)
- mOutputStream.write(IMAGEID_FIELD_SIZE)
- mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId)))
+ // URL
+ mOutputStream.write(URL_FIELD_TYPE)
+ StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.url)
- // Title
- //byte[] title = mEntry.title.getBytes("UTF-8");
- mOutputStream.write(TITLE_FIELD_TYPE)
- length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong()
+ // Username
+ mOutputStream.write(USERNAME_FIELD_TYPE)
+ StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.username)
- // URL
- mOutputStream.write(URL_FIELD_TYPE)
- length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong()
+ // Password
+ mOutputStream.write(PASSWORD_FIELD_TYPE)
+ writePassword(mEntry.password, mOutputStream)
- // Username
- mOutputStream.write(USERNAME_FIELD_TYPE)
- length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong()
+ // Additional
+ mOutputStream.write(ADDITIONAL_FIELD_TYPE)
+ StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.notes)
- // Password
- mOutputStream.write(PASSWORD_FIELD_TYPE)
- length += writePassword(mEntry.password, mOutputStream).toLong()
+ // Create date
+ writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
- // Additional
- mOutputStream.write(ADDITIONAL_FIELD_TYPE)
- length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong()
+ // Modification date
+ writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
- // Create date
- writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
+ // Access date
+ writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
- // Modification date
- writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
+ // Expiration date
+ writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
- // Access date
- writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
+ // Binary description
+ mOutputStream.write(BINARY_DESC_FIELD_TYPE)
+ StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription)
- // Expiration date
- writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
-
- // Binary description
- mOutputStream.write(BINARY_DESC_FIELD_TYPE)
- length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong()
-
- // Binary
- mOutputStream.write(BINARY_DATA_FIELD_TYPE)
- val binaryData = mEntry.binaryData
- val binaryDataLength = binaryData?.length() ?: 0L
- // Write data length
- mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
- // Write data
- if (binaryDataLength > 0) {
- binaryData?.getInputDataStream().use { inputStream ->
- inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
- length += buffer.size
- mOutputStream.write(buffer)
+ // Binary
+ mOutputStream.write(BINARY_DATA_FIELD_TYPE)
+ val binaryData = mEntry.binaryData
+ val binaryDataLength = binaryData?.length ?: 0L
+ // Write data length
+ mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
+ // Write data
+ if (binaryDataLength > 0) {
+ binaryData?.getInputDataStream(mCipherKey).use { inputStream ->
+ inputStream?.readAllBytes { buffer ->
+ mOutputStream.write(buffer)
+ }
+ inputStream?.close()
}
- inputStream?.close()
}
- }
- // End
- mOutputStream.write(END_FIELD_TYPE)
- mOutputStream.write(ZERO_FIELD_SIZE)
+ // End
+ mOutputStream.write(END_FIELD_TYPE)
+ mOutputStream.write(ZERO_FIELD_SIZE)
+ } catch (e: IOException) {
+ throw DatabaseOutputException("Failed to output an entry.", e)
+ }
}
@Throws(IOException::class)
@@ -144,29 +139,27 @@ class EntryOutputKDB
companion object {
// Constants
- val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
- val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
- val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
- val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
- val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
- val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
- val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
- val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
- val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
- val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
- val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
- val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
- val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
- val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
- val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
+ private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
+ private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
+ private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
+ private val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
+ private val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
+ private val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
+ private val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
+ private val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
+ private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
+ private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10)
+ private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11)
+ private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12)
+ private val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13)
+ private val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14)
+ private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
- val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
- val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
- val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
- val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
+ private val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16))
+ private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
+ private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
+ private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
+ private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
+ private val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt
index d1e157fa3..326dcf543 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt
@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.file.output
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.uIntTo4Bytes
import com.kunzisoft.keepass.stream.uShortTo2Bytes
@@ -31,79 +32,84 @@ import java.io.OutputStream
/**
* 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() {
- //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
- mOutputStream.write(GROUPID_FIELD_TYPE)
- mOutputStream.write(GROUPID_FIELD_SIZE)
- mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id)))
+ // Group ID
+ mOutputStream.write(GROUPID_FIELD_TYPE)
+ mOutputStream.write(GROUPID_FIELD_SIZE)
+ mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id)))
- // Name
- mOutputStream.write(NAME_FIELD_TYPE)
- StringDatabaseKDBUtils.writeStringToBytes(mGroup.title, mOutputStream)
+ // Name
+ mOutputStream.write(NAME_FIELD_TYPE)
+ StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mGroup.title)
- // Create date
- mOutputStream.write(CREATE_FIELD_TYPE)
- mOutputStream.write(DATE_FIELD_SIZE)
- mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
+ // Create date
+ mOutputStream.write(CREATE_FIELD_TYPE)
+ mOutputStream.write(DATE_FIELD_SIZE)
+ mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
- // Modification date
- mOutputStream.write(MOD_FIELD_TYPE)
- mOutputStream.write(DATE_FIELD_SIZE)
- mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
+ // Modification date
+ mOutputStream.write(MOD_FIELD_TYPE)
+ mOutputStream.write(DATE_FIELD_SIZE)
+ mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
- // Access date
- mOutputStream.write(ACCESS_FIELD_TYPE)
- mOutputStream.write(DATE_FIELD_SIZE)
- mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
+ // Access date
+ mOutputStream.write(ACCESS_FIELD_TYPE)
+ mOutputStream.write(DATE_FIELD_SIZE)
+ mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
- // Expiration date
- mOutputStream.write(EXPIRE_FIELD_TYPE)
- mOutputStream.write(DATE_FIELD_SIZE)
- mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
+ // Expiration date
+ mOutputStream.write(EXPIRE_FIELD_TYPE)
+ mOutputStream.write(DATE_FIELD_SIZE)
+ mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
- // Image ID
- mOutputStream.write(IMAGEID_FIELD_TYPE)
- mOutputStream.write(IMAGEID_FIELD_SIZE)
- mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId)))
+ // Image ID
+ mOutputStream.write(IMAGEID_FIELD_TYPE)
+ mOutputStream.write(IMAGEID_FIELD_SIZE)
+ mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId)))
- // Level
- mOutputStream.write(LEVEL_FIELD_TYPE)
- mOutputStream.write(LEVEL_FIELD_SIZE)
- mOutputStream.write(uShortTo2Bytes(mGroup.level))
+ // Level
+ mOutputStream.write(LEVEL_FIELD_TYPE)
+ mOutputStream.write(LEVEL_FIELD_SIZE)
+ mOutputStream.write(uShortTo2Bytes(mGroup.level))
- // Flags
- mOutputStream.write(FLAGS_FIELD_TYPE)
- mOutputStream.write(FLAGS_FIELD_SIZE)
- mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags)))
+ // Flags
+ mOutputStream.write(FLAGS_FIELD_TYPE)
+ mOutputStream.write(FLAGS_FIELD_SIZE)
+ mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags)))
- // End
- mOutputStream.write(END_FIELD_TYPE)
- mOutputStream.write(ZERO_FIELD_SIZE)
+ // End
+ mOutputStream.write(END_FIELD_TYPE)
+ mOutputStream.write(ZERO_FIELD_SIZE)
+ } catch (e: IOException) {
+ throw DatabaseOutputException("Failed to output a group.", e)
+ }
}
companion object {
// Constants
- val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
- val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
- val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
- val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
- val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
- val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
- val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
- val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
- val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
- val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
+ private val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1)
+ private val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)
+ private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3)
+ private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4)
+ private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5)
+ private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6)
+ private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7)
+ private val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8)
+ private val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9)
+ private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF)
- val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
- val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2))
- val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
- val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
+ private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
+ private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5))
+ private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
+ private val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2))
+ private val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4))
+ private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0))
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
index 553641f20..5dc9730be 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.EntryInfo
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.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt
index 02257d9ba..26b9ed5e4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt
@@ -29,19 +29,22 @@ import com.kunzisoft.keepass.utils.writeEnum
data class EntryAttachmentState(var attachment: Attachment,
var streamDirection: StreamDirection,
var downloadState: AttachmentState = AttachmentState.NULL,
- var downloadProgression: Int = 0) : Parcelable {
+ var downloadProgression: Int = 0,
+ var previewState: AttachmentState = AttachmentState.NULL) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
parcel.readEnum() ?: StreamDirection.DOWNLOAD,
parcel.readEnum() ?: AttachmentState.NULL,
- parcel.readInt())
+ parcel.readInt(),
+ parcel.readEnum() ?: AttachmentState.NULL)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(attachment, flags)
parcel.writeEnum(streamDirection)
parcel.writeEnum(downloadState)
parcel.writeInt(downloadProgression)
+ parcel.writeEnum(previewState)
}
override fun describeContents(): Int {
@@ -73,5 +76,5 @@ data class EntryAttachmentState(var attachment: Attachment,
}
enum class AttachmentState {
- NULL, START, IN_PROGRESS, COMPLETE, ERROR
+ NULL, START, IN_PROGRESS, COMPLETE, CANCELED, ERROR
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
index 73f2ab8f2..32ff5b3e9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
@@ -23,44 +23,28 @@ import android.os.Parcel
import android.os.Parcelable
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.icon.IconImage
-import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
-import kotlin.collections.ArrayList
-class EntryInfo : Parcelable {
+class EntryInfo : NodeInfo {
var id: String = ""
- var title: String = ""
- var icon: IconImage = IconImageStandard()
var username: 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 notes: String = ""
var customFields: List = ArrayList()
var attachments: List = ArrayList()
var otpModel: OtpModel? = null
- constructor()
+ constructor(): super()
- private constructor(parcel: Parcel) {
+ constructor(parcel: Parcel): super(parcel) {
id = parcel.readString() ?: id
- title = parcel.readString() ?: title
- icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
username = parcel.readString() ?: username
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
notes = parcel.readString() ?: notes
parcel.readList(customFields, Field::class.java.classLoader)
@@ -73,15 +57,10 @@ class EntryInfo : Parcelable {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
+ super.writeToParcel(parcel, flags)
parcel.writeString(id)
- parcel.writeString(title)
- parcel.writeParcelable(icon, flags)
parcel.writeString(username)
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(notes)
parcel.writeArray(customFields.toTypedArray())
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt
new file mode 100644
index 000000000..9c4890ac8
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt
@@ -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 {
+ override fun createFromParcel(parcel: Parcel): GroupInfo {
+ return GroupInfo(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt
new file mode 100644
index 000000000..e5ee12c72
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt
@@ -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 {
+ override fun createFromParcel(parcel: Parcel): MainCredential {
+ return MainCredential(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt
new file mode 100644
index 000000000..9c2a1d252
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt
@@ -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 {
+ override fun createFromParcel(parcel: Parcel): NodeInfo {
+ return NodeInfo(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt
index bddb8a284..5244fac28 100644
--- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt
@@ -210,13 +210,13 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun isValidBase32(secret: String): Boolean {
val secretChars = replaceBase32Chars(secret)
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 {
// TODO replace base 64 chars
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 {
diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt
index 92121640c..9e382b39a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt
@@ -354,9 +354,15 @@ object OtpEntryFields {
return false
}
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
- otpElement.tokenType = matcher.group(2)?.let {
- OtpTokenType.getFromString(it)
- } ?: OtpTokenType.RFC6238
+ matcher.group(2)?.let { secondMatcher ->
+ try {
+ otpElement.digits = secondMatcher.toInt()
+ } catch (e: NumberFormatException) {
+ otpElement.digits = OTP_DEFAULT_DIGITS
+ otpElement.tokenType = OtpTokenType.getFromString(secondMatcher)
+ }
+ }
+
}
} catch (exception: Exception) {
return false
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt
index 7974e24bb..26c56d01f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt
@@ -1,4 +1,4 @@
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt
similarity index 75%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt
index 341f78f22..adfd276ca 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.ContentResolver
@@ -29,15 +29,14 @@ import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
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.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
-import com.kunzisoft.keepass.stream.readBytes
-import com.kunzisoft.keepass.timeout.TimeoutHelper
+import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.*
-import java.io.BufferedInputStream
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
@@ -100,12 +99,15 @@ class AttachmentFileNotificationService: LockNotificationService() {
when(intent?.action) {
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
- actionUploadOrDownload(downloadFileUri,
+ actionStartUploadOrDownload(downloadFileUri,
intent,
StreamDirection.UPLOAD)
}
+ ACTION_ATTACHMENT_FILE_STOP_UPLOAD -> {
+ actionStopUpload()
+ }
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
- actionUploadOrDownload(downloadFileUri,
+ actionStartUploadOrDownload(downloadFileUri,
intent,
StreamDirection.DOWNLOAD)
}
@@ -215,15 +217,22 @@ class AttachmentFileNotificationService: LockNotificationService() {
setDeleteIntent(pendingDeleteIntent)
setOngoing(false)
}
+ AttachmentState.CANCELED -> {
+ setContentText(getString(R.string.download_canceled))
+ setDeleteIntent(pendingDeleteIntent)
+ setOngoing(false)
+ }
AttachmentState.ERROR -> {
setContentText(getString(R.string.error_file_not_create))
+ setDeleteIntent(pendingDeleteIntent)
setOngoing(false)
}
}
}
when (attachmentNotification.entryAttachmentState.downloadState) {
- AttachmentState.ERROR,
- AttachmentState.COMPLETE -> {
+ AttachmentState.COMPLETE,
+ AttachmentState.CANCELED,
+ AttachmentState.ERROR -> {
stopForeground(false)
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
} else -> {
@@ -234,6 +243,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
override fun onDestroy() {
attachmentNotificationList.forEach { attachmentNotification ->
+ attachmentNotification.attachmentFileAction?.cancel()
attachmentNotification.attachmentFileAction?.listener = null
notificationManager?.cancel(attachmentNotification.notificationId)
}
@@ -262,10 +272,10 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
- private fun actionUploadOrDownload(downloadFileUri: Uri?,
- intent: Intent,
- streamDirection: StreamDirection) {
- if (downloadFileUri != null
+ private fun actionStartUploadOrDownload(fileUri: Uri?,
+ intent: Intent,
+ streamDirection: StreamDirection) {
+ if (fileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
try {
intent.getParcelableExtra(ATTACHMENT_KEY)?.let { entryAttachment ->
@@ -273,7 +283,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
- val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState)
+ val attachmentNotification = AttachmentNotification(fileUri, nextNotificationId, entryAttachmentState)
// Add action to the list on start
attachmentNotificationList.add(attachmentNotification)
@@ -286,11 +296,24 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
} 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 val attachmentNotification: AttachmentNotification,
private val contentResolver: ContentResolver) {
@@ -307,8 +330,6 @@ class AttachmentFileNotificationService: LockNotificationService() {
// on pre execute
CoroutineScope(Dispatchers.Main).launch {
- TimeoutHelper.temporarilyDisableTimeout()
-
attachmentNotification.attachmentFileAction = this@AttachmentFileAction
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.START
@@ -319,70 +340,81 @@ class AttachmentFileNotificationService: LockNotificationService() {
withContext(Dispatchers.IO) {
// on Progress with thread
- val asyncResult: Deferred = async {
- var progressResult = true
- try {
- attachmentNotification.entryAttachmentState.apply {
- downloadState = AttachmentState.IN_PROGRESS
+ val asyncAction = launch {
+ attachmentNotification.entryAttachmentState.apply {
+ try {
+ downloadState = AttachmentState.IN_PROGRESS
- when (streamDirection) {
- StreamDirection.UPLOAD -> {
- uploadToDatabase(
- attachmentNotification.uri,
- attachment.binaryAttachment,
- contentResolver, 1024) { percent ->
- publishProgress(percent)
+ when (streamDirection) {
+ StreamDirection.UPLOAD -> {
+ uploadToDatabase(
+ attachmentNotification.uri,
+ attachment.binaryAttachment,
+ contentResolver, 1024,
+ { // Cancellation
+ downloadState == AttachmentState.CANCELED
+ }
+ ) { percent ->
+ publishProgress(percent)
+ }
+ }
+ StreamDirection.DOWNLOAD -> {
+ downloadFromDatabase(
+ attachmentNotification.uri,
+ attachment.binaryAttachment,
+ contentResolver, 1024) { percent ->
+ publishProgress(percent)
+ }
}
}
- StreamDirection.DOWNLOAD -> {
- downloadFromDatabase(
- attachmentNotification.uri,
- attachment.binaryAttachment,
- contentResolver, 1024) { percent ->
- publishProgress(percent)
- }
- }
- }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ downloadState = AttachmentState.ERROR
}
- } catch (e: Exception) {
- Log.e(TAG, "Unable to upload or download file", e)
- progressResult = false
}
- progressResult
+ attachmentNotification.entryAttachmentState.downloadState
}
// on post execute
withContext(Dispatchers.Main) {
- val result = asyncResult.await()
- attachmentNotification.attachmentFileAction = null
+ asyncAction.join()
attachmentNotification.entryAttachmentState.apply {
- downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
- downloadProgression = 100
+ if (downloadState != AttachmentState.CANCELED
+ && downloadState != AttachmentState.ERROR) {
+ downloadState = AttachmentState.COMPLETE
+ downloadProgression = 100
+ }
}
+ attachmentNotification.attachmentFileAction = null
listener?.onUpdate(attachmentNotification)
- TimeoutHelper.releaseTemporarilyDisableTimeout()
}
}
}
+ fun cancel() {
+ attachmentNotification.entryAttachmentState.downloadState = AttachmentState.CANCELED
+ }
+
fun downloadFromDatabase(attachmentToUploadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0L
- val fileSize = binaryAttachment.length()
+ val fileSize = binaryAttachment.length
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
- binaryAttachment.getUnGzipInputDataStream().use { inputStream ->
- inputStream.readBytes(bufferSize) { buffer ->
- outputStream.write(buffer)
- dataDownloaded += buffer.size
- try {
- val percentDownload = (100 * dataDownloaded / fileSize).toInt()
- update?.invoke(percentDownload)
- } catch (e: Exception) {
- Log.e(TAG, "", e)
+ Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
+ binaryAttachment.getUnGzipInputDataStream(binaryCipherKey).use { inputStream ->
+ inputStream.readAllBytes(bufferSize) { buffer ->
+ outputStream.write(buffer)
+ dataDownloaded += buffer.size
+ try {
+ val percentDownload = (100 * dataDownloaded / fileSize).toInt()
+ update?.invoke(percentDownload)
+ } catch (e: Exception) {
+ Log.e(TAG, "", e)
+ }
}
}
}
@@ -393,13 +425,14 @@ class AttachmentFileNotificationService: LockNotificationService() {
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
+ canceled: ()-> Boolean = { false },
update: ((percent: Int)->Unit)? = null) {
var dataUploaded = 0L
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
- UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream ->
- binaryAttachment.getGzipOutputDataStream().use { outputStream ->
- BufferedInputStream(inputStream).use { attachmentBufferedInputStream ->
- attachmentBufferedInputStream.readBytes(bufferSize) { buffer ->
+ UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream ->
+ Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
+ binaryAttachment.getGzipOutputDataStream(binaryCipherKey).use { outputStream ->
+ inputStream.readAllBytes(bufferSize, canceled) { buffer ->
outputStream.write(buffer)
dataUploaded += buffer.size
try {
@@ -419,7 +452,9 @@ class AttachmentFileNotificationService: LockNotificationService() {
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachmentState.apply {
- downloadState = AttachmentState.IN_PROGRESS
+ if (downloadState != AttachmentState.CANCELED) {
+ downloadState = AttachmentState.IN_PROGRESS
+ }
downloadProgression = percent
}
CoroutineScope(Dispatchers.Main).launch {
@@ -441,6 +476,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
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_STOP_UPLOAD = "ACTION_ATTACHMENT_FILE_STOP_UPLOAD"
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt
index 61dfbcfd3..2521db4af 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.os.Parcel
import android.os.Parcelable
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
index d0678e7d8..2930f5a78 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
similarity index 94%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
index cd1cb6efb..14de85eb5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.app.PendingIntent
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.NodeId
import com.kunzisoft.keepass.database.element.node.Type
+import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -141,9 +142,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val conditionExists = previousDatabaseInfo != null
&& previousDatabaseInfo.exists != lastFileDatabaseInfo.exists
// To prevent dialog opening too often
+ // Add 10 seconds delta time to prevent spamming
val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null
&& oldDatabaseModification < newDatabaseModification
- && mLastLocalSaveTime + 5000 < newDatabaseModification)
+ && mLastLocalSaveTime + 10000 < newDatabaseModification)
if (conditionExists || conditionLastModification) {
// Show the dialog only if it's real new info and not a delay after a save
@@ -399,10 +401,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
intent?.removeExtra(DATABASE_TASK_WARNING_KEY)
intent?.removeExtra(DATABASE_URI_KEY)
- intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY)
- intent?.removeExtra(MASTER_PASSWORD_KEY)
- intent?.removeExtra(KEY_FILE_CHECKED_KEY)
- intent?.removeExtra(KEY_FILE_URI_KEY)
+ intent?.removeExtra(MAIN_CREDENTIAL_KEY)
intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
@@ -474,13 +473,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
- && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
- && intent.hasExtra(MASTER_PASSWORD_KEY)
- && intent.hasExtra(KEY_FILE_CHECKED_KEY)
- && intent.hasExtra(KEY_FILE_URI_KEY)
+ && intent.hasExtra(MAIN_CREDENTIAL_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)
return null
@@ -490,14 +486,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseUri,
getString(R.string.database_default_name),
getString(R.string.database),
- intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
- intent.getStringExtra(MASTER_PASSWORD_KEY),
- intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
- keyFileUri
+ mainCredential
) { result ->
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
- putParcelable(KEY_FILE_URI_KEY, keyFileUri)
+ putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
}
}
} else {
@@ -508,15 +501,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
- && intent.hasExtra(MASTER_PASSWORD_KEY)
- && intent.hasExtra(KEY_FILE_URI_KEY)
+ && intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
- val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
- val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
+ val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
@@ -527,8 +518,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
this,
mDatabase,
databaseUri,
- masterPassword,
- keyFileUri,
+ mainCredential,
readOnly,
cipherEntity,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
@@ -537,8 +527,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Add each info to reload database after thrown duplicate UUID exception
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
- putString(MASTER_PASSWORD_KEY, masterPassword)
- putParcelable(KEY_FILE_URI_KEY, keyFileUri)
+ putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
}
@@ -561,19 +550,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY)
- && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
- && intent.hasExtra(MASTER_PASSWORD_KEY)
- && intent.hasExtra(KEY_FILE_CHECKED_KEY)
- && intent.hasExtra(KEY_FILE_URI_KEY)
+ && intent.hasExtra(MAIN_CREDENTIAL_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this,
mDatabase,
databaseUri,
- intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
- intent.getStringExtra(MASTER_PASSWORD_KEY),
- intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
- intent.getParcelableExtra(KEY_FILE_URI_KEY)
+ intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
)
} else {
null
@@ -896,10 +879,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
- const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_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 MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
index 1659bb23d..af214e50b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt
similarity index 97%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt
index 2bb4889ec..daf37a124 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.content.Intent
import com.kunzisoft.keepass.utils.LockReceiver
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt
similarity index 98%
rename from app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt
rename to app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt
index 48b7106c0..e475ad4eb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt
@@ -1,4 +1,4 @@
-package com.kunzisoft.keepass.notifications
+package com.kunzisoft.keepass.services
import android.app.NotificationChannel
import android.app.NotificationManager
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
index 2c4fe2f3e..b9ebbab67 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
@@ -44,7 +44,7 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education
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.utils.UriUtil
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
index f5a50c05a..fe9a26dc9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
@@ -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.CompressionAlgorithm
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.preferencedialogfragment.*
import com.kunzisoft.keepass.tasks.ActionRunnable
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
index 7c6e2671a..4760e86c2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
@@ -36,9 +36,10 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
-import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
+import com.kunzisoft.keepass.model.MainCredential
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.timeout.TimeoutHelper
-import com.kunzisoft.keepass.view.showActionError
+import com.kunzisoft.keepass.view.showActionErrorIfNeeded
open class SettingsActivity
: LockingActivity(),
@@ -98,18 +99,23 @@ open class SettingsActivity
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
- startActivity(intent)
- finish()
- overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
+ if (result.isSuccess) {
+ startActivity(intent)
+ finish()
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
+ } else {
+ this.showActionErrorIfNeeded(result)
+ finish()
+ }
}
else -> {
// Call result in fragment
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
?.onProgressDialogThreadResult(actionTask, result)
- coordinatorLayout?.showActionError(result)
}
}
+ coordinatorLayout?.showActionErrorIfNeeded(result)
}
// To reload the current screen
@@ -136,52 +142,33 @@ open class SettingsActivity
}
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
- masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?) {
+ mainCredential: MainCredential) {
databaseUri?.let {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
- masterPasswordChecked,
- masterPassword,
- keyFileChecked,
- keyFile
+ mainCredential
)
}
}
- override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?) {
+ override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
Database.getInstance().let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
- if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
+ if (database.validatePasswordEncoding(mainCredential)) {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
- masterPasswordChecked,
- masterPassword,
- keyFileChecked,
- keyFile
+ mainCredential
)
} else {
- PasswordEncodingDialogFragment.getInstance(databaseUri,
- masterPasswordChecked,
- masterPassword,
- keyFileChecked,
- keyFile
- ).show(supportFragmentManager, "passwordEncodingTag")
+ PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
+ .show(supportFragmentManager, "passwordEncodingTag")
}
}
}
}
- override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean,
- masterPassword: String?,
- keyFileChecked: Boolean,
- keyFile: Uri?) {}
+ override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
if (PreferencesUtil.showLockDatabaseButton(this)) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt b/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt
index 4e01ecf3a..e8ede8248 100644
--- a/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt
@@ -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.
*/
@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)
var read = 0
- while (read != -1) {
+ while (read != -1 && !cancelCondition()) {
read = this.read(buffer, 0, buffer.size)
if (read != -1) {
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.
*/
@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 buffer = ByteArray(bufferLength)
diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt
index b071c33d6..933a47536 100644
--- a/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt
@@ -31,10 +31,11 @@ import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
-import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
-import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
-import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
-import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE
+import com.kunzisoft.keepass.services.AttachmentFileNotificationService
+import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
+import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
+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) {
@@ -120,6 +121,10 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
}, ACTION_ATTACHMENT_FILE_START_UPLOAD)
}
+ fun stopUploadAllAttachments() {
+ start(null, ACTION_ATTACHMENT_FILE_STOP_UPLOAD)
+ }
+
fun startDownloadAttachment(downloadFileUri: Uri,
attachment: Attachment) {
start(Bundle().apply {
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
index 3dcfcf7b4..c12fb0838 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
@@ -32,8 +32,8 @@ import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.magikeyboard.MagikIME
-import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
-import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
+import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
+import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt b/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt
index 611e97b34..229487c8c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt
@@ -53,12 +53,12 @@ object StringDatabaseKDBUtils {
}
@Throws(IOException::class)
- fun writeStringToBytes(string: String?, os: OutputStream): Int {
+ fun writeStringToStream(outputStream: OutputStream, string: String?): Int {
var str = string
if (str == null) {
// Write out a null character
- os.write(uIntTo4Bytes(UnsignedInt(1)))
- os.write(0x00)
+ outputStream.write(uIntTo4Bytes(UnsignedInt(1)))
+ outputStream.write(0x00)
return 0
}
@@ -69,9 +69,9 @@ object StringDatabaseKDBUtils {
val initial = str.toByteArray(defaultCharset)
val length = initial.size + 1
- os.write(uIntTo4Bytes(UnsignedInt(length)))
- os.write(initial)
- os.write(0x00)
+ outputStream.write(uIntTo4Bytes(UnsignedInt(length)))
+ outputStream.write(initial)
+ outputStream.write(0x00)
return length
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt
index e54399b2a..e7114ef75 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
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.Entry
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) {
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt b/app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt
new file mode 100644
index 000000000..56ae257f0
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt
@@ -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 .
+ *
+ */
+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()
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt
index 2e25e9192..cc86a0296 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt
@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.R
class ToolbarAction @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
- defStyle: Int = androidx.appcompat.R.attr.toolbarStyle)
+ defStyle: Int = R.attr.actionToolbarAppearance)
: Toolbar(context, attrs, defStyle) {
private var mActionModeCallback: ActionMode.Callback? = null
@@ -39,7 +39,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
private var isOpen = false
init {
- visibility = View.GONE
+ setNavigationIcon(R.drawable.ic_close_white_24dp)
}
fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode {
@@ -55,8 +55,6 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
actionMode.finish()
}
- setNavigationIcon(R.drawable.ic_close_white_24dp)
-
open()
return actionMode
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt
index 720b6cfd7..df734a796 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.view
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
+import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
@@ -35,6 +36,7 @@ import android.text.style.ClickableSpan
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
+import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.updatePadding
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) {
result.exception?.errorId?.let { errorId ->
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show()
diff --git a/app/src/main/res/drawable/keepassdx_logo.png b/app/src/main/res/drawable/keepassdx_logo.png
new file mode 100644
index 000000000..591f7e050
Binary files /dev/null and b/app/src/main/res/drawable/keepassdx_logo.png differ
diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml
index 02d72bc52..93f4eda35 100644
--- a/app/src/main/res/layout/activity_entry.xml
+++ b/app/src/main/res/layout/activity_entry.xml
@@ -67,7 +67,7 @@
android:layout_height="?attr/actionBarSize"
android:background="@color/transparent"
android:theme="?attr/toolbarAppearance"
- android:popupTheme="?attr/toolbarPopupAppearance"
+ app:popupTheme="?attr/toolbarPopupAppearance"
app:layout_collapseMode="pin"
tools:targetApi="lollipop">
diff --git a/app/src/main/res/layout/activity_entry_edit.xml b/app/src/main/res/layout/activity_entry_edit.xml
index e04e5ac54..c24debc93 100644
--- a/app/src/main/res/layout/activity_entry_edit.xml
+++ b/app/src/main/res/layout/activity_entry_edit.xml
@@ -33,27 +33,14 @@
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
-
-
-
-
-
+ 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" />
@@ -79,24 +66,23 @@
-
+ app:popupTheme="?attr/toolbarPopupAppearance" />
diff --git a/app/src/main/res/layout/activity_group.xml b/app/src/main/res/layout/activity_group.xml
index 8c56fdb1b..8073ea918 100644
--- a/app/src/main/res/layout/activity_group.xml
+++ b/app/src/main/res/layout/activity_group.xml
@@ -64,7 +64,7 @@
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance"
- android:popupTheme="?attr/toolbarPopupAppearance"
+ app:popupTheme="?attr/toolbarPopupAppearance"
android:elevation="4dp"
tools:targetApi="lollipop">
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_password.xml
index 606225498..ddb806596 100644
--- a/app/src/main/res/layout/activity_password.xml
+++ b/app/src/main/res/layout/activity_password.xml
@@ -80,7 +80,7 @@
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance"
- android:popupTheme="?attr/toolbarPopupAppearance"
+ app:popupTheme="?attr/toolbarPopupAppearance"
app:layout_collapseMode="pin"
tools:targetApi="lollipop">
-
-
+
-
-
-
-
-
+ android:layout_height="wrap_content" />
.
-->
-
-
-
+
-
+
+
+
+
+
+
+
+
-
-
-
+ android:layout_marginStart="4dp" />
+
+
diff --git a/app/src/main/res/layout/item_attachment.xml b/app/src/main/res/layout/item_attachment.xml
index 056f57b6e..95d69f18b 100644
--- a/app/src/main/res/layout/item_attachment.xml
+++ b/app/src/main/res/layout/item_attachment.xml
@@ -17,111 +17,138 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see .
-->
-
-
-
+
+
-
+ android:layout_alignBottom="@+id/item_attachment_thumbnail"
+ android:background="?attr/cardBackgroundTransparentColor">
+
+
+
-
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
-
+
+
+ android:focusable="false">
+
+
+
+
+
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_default.xml b/app/src/main/res/layout/toolbar_default.xml
index 87d0a97f2..7f83d9a4a 100644
--- a/app/src/main/res/layout/toolbar_default.xml
+++ b/app/src/main/res/layout/toolbar_default.xml
@@ -20,12 +20,13 @@
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_expiration.xml b/app/src/main/res/layout/view_expiration.xml
new file mode 100644
index 000000000..18fd548d2
--- /dev/null
+++ b/app/src/main/res/layout/view_expiration.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 4975e880a..8ef8d10d9 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -553,4 +553,9 @@
Databázi nově načíst
Přepsat externí změny uložením databáze nebo databázi včetně posledních změn nově načíst.
Informace obsažená ve Vašem databázovém souboru by změněna mimo aplikaci.
+ GiB
+ MiB
+ KiB
+ B
+ Nemohu rozpoznat existující typ OTP v této formě, jeho validace patrně nebude generovat správný token.
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index fd44ec980..3c010d470 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -450,7 +450,8 @@
- Klassisch Dunkel
- Himmel und Meer
- Roter Vulkan
- - Purple Pro
+ - Purple Light
+ - Purple Dark
Datei Schreibrechte gewähren, um Datenbankänderungen zu speichern
Einrichten einer Einmal-Passwortverwaltung (HOTP / TOTP), um ein Token zu generieren, das für die Zwei-Faktor-Authentifizierung (2FA) angefordert wird.
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 7dee954af..af2c5bfc8 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -38,7 +38,7 @@
Διάρκεια αποθήκευσης στο πρόχειρο
Επιλέξτε για αντιγραφή %1$s στο πρόχειρο
Ανάκτηση κλειδιού βάσης δεδομένων…
- Βάση Δεδομένων
+ Βάση δεδομένων
Αποκρυπτογράφηση περιεχομένου βάσης δεδομένων …
Χρήση ως προεπιλεγμένης βάσης δεδομένων
Ψηφία
@@ -46,7 +46,7 @@
\nΠαρέχεται ως έχει, με άδεια <strong>GPLv3</strong>, χωρίς καμία εγγύηση.
Ανοίξτε την υπάρχουσα βάση δεδομένων
Πρόσβαση
- Ακύρωση
+ Άκυρο
Σημειώσεις
Επιβεβαίωση κωδικού
Δημιουργήθηκε
@@ -54,11 +54,11 @@
Αρχείο-Κλειδί
Τροποποιήθηκε
Δεν ήταν δυνατή η εύρεση δεδομένων εισόδου.
- Κωδικός Πρόσβασης
+ Κωδικός πρόσβασης
Αποθήκευση
Τίτλος
Διεύθυνση URL
- Όνομα Χρήστη
+ Όνομα χρήστη
Η ροή κρυπτογράφησης Arcfour δεν υποστηρίζεται.
Το KeePassDX δε μπορεί να χειριστεί αυτή τη διεύθυνση URI.
Δεν ήταν δυνατή η δημιουργία αρχείου
@@ -82,7 +82,7 @@
Αρχείο κλειδί
Μήκος
Κωδικός
- Κωδικός Πρόσβασης
+ Κωδικός πρόσβασης
Δεν ήταν δυνατή η ανάγνωση διαπιστευτηρίων.
Λάθος αλγόριθμος.
Δεν ήταν δυνατή η αναγνώριση της μορφής της βάσης δεδομένων.
@@ -258,7 +258,7 @@
Ευχαριστούμε πολύ για τη συνεισφορά σας.
Εργαζόμαστε σκληρά για να διαθέσουμε αυτό το χαρακτηριστικό γρήγορα.
Θυμηθείτε να ενημερώνετε την εφαρμογή σας, εγκαθιστώντας νέες εκδόσεις.
- Download
+ Λήψη
Συνεισφορά
ChaCha20
AES
@@ -270,7 +270,7 @@
Αντιγραφή
Μετακίνηση
Επικόλληση
- Ακύρωση
+ Άκυρο
Εάν αποτύχει η αυτόματη διαγραφή του προχείρου, διαγράψτε το ιστορικό του χειροκίνητα.
Προειδοποίηση: Το πρόχειρο μοιράζεται από όλες τις εφαρμογές. Αν αντιγράφονται ευαίσθητα δεδομένα, άλλο λογισμικό μπορεί να το ανακτήσει.
Να μην επιτρέπεται κανένα κύριο κλειδί
@@ -552,4 +552,10 @@
Αντικαταστήστε τις εξωτερικές τροποποιήσεις αποθηκεύοντας τη βάση δεδομένων ή φορτώστε την ξανά με τις πιο πρόσφατες αλλαγές.
Οι πληροφορίες που περιέχονται στο αρχείο της βάσης δεδομένων σας έχουν τροποποιηθεί εκτός της εφαρμογής.
Επαναφόρτωση βάσης δεδομένων
+ GiB
+ MiB
+ KiB
+ B
+ Ο υπάρχων τύπος OTP δεν αναγνωρίζεται από αυτήν τη φόρμα, η επικύρωσή του ενδέχεται να μην δημιουργεί πλέον σωστά το token.
+ Ακυρώθηκε!
\ No newline at end of file
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
new file mode 100644
index 000000000..b470d8700
--- /dev/null
+++ b/app/src/main/res/values-eo/strings.xml
@@ -0,0 +1,150 @@
+
+
+ Aspekto
+ Historio
+ Versio %1$s
+ Averto
+ Majuskloj
+ Serĉrezultoj
+ Serĉi
+ Uzantnomo
+ Titolo
+ Naturala ordo
+ Ordigi
+ Serĉi
+ Ne ĉesigu la aplikaĵon…
+ Datumbaza konservado…
+ Paralelado
+ Memoruzado
+ Elektreĝimo
+ Konservreĝimo
+ Serĉreĝimo
+ Kontraŭskribe protektita
+ Protekto
+ Laborado…
+ Nova datumbaza kreado…
+ Subdomajna serĉo
+ Rapida serĉo
+ Krei novan datumbazon
+ Malfermi ekzistantan datumbazon
+ Instalu TTT-legilon por malfermi ĉi tiun retadreson.
+ Nieniu serĉrezulto
+ Neniam
+ Minuso
+ Forigi historion
+ Historia restaŭro
+ Malplenigi la rubujon
+ Modifebla
+ Kontraŭskribe protektita
+ Iri al retadreso
+ Montri pasvorton
+ Serĉi
+ Malfermi
+ Reŝargi datumbazon
+ Konservi datumbazon
+ Ŝlosi datumbazon
+ Kaŝi pasvorton
+ Nuligi
+ Forigi
+ Algui
+ Movi
+ Kopii
+ Redakti
+ Sekureca agordoj
+ Datumbaza agordoj
+ Minusklo
+ Teksta grandeco en la elementa listo
+ Aplikaĵaj agordoj
+ Agordoj
+ Kopio de %1$s
+ Pri
+ Kaŝi pasvortojn sub (***) defaŭlte
+ Kaŝi pasvortojn
+ Datumbaza ŝarĝado…
+ Datumbaza kreado…
+ Vidigi uzantnomojn en elementaj listoj
+ Vidigi uzantnomojn
+ Longeco
+ La ŝlosila dosiero estas malplena.
+ Malĝusta algoritmo.
+ Pasvorto
+ Pasvorto
+ Longeco
+ Ŝlosila dosiero
+ Grupa nomo
+ Generita pasvorto
+ Konfirmu pasvorton
+ Generi pasvorton
+ Dosiermastrumilo
+ Kampa valoro
+ Kampa nomo
+ La kampa nomo jam ekzistas.
+ Ne povis konservi datumbazon.
+ Vi ne povas kopii grupon ĉi tie.
+ Vi ne povas kopii elementon ĉi tie.
+ Vi ne povas movigi elementon ĉi tie.
+ Ĉi tiu etikedo jam ekzistas.
+ La pasvortoj ne similas.
+ Ne povas ŝarĝi vian datumbazon.
+ Elektu ŝlosilan dosieron.
+ Tajpu nomon.
+ Certigu ke la vojo estas ĝusta.
+ Ne povas legi datumbazon.
+ Ne povas krei dosieron
+ Uzantnomo
+ Retadreso
+ Titolo
+ Konservi
+ Pasvorto
+ Ne povas trovi ian elementan datumon.
+ Modifita
+ Ŝlosila dosiero
+ Aldonaĵoj
+ Historio
+ Senvalidiĝos
+ Kreita
+ Konfirmu pasvorton
+ Notoj
+ Rezigni
+ Ciferoj
+ Uzi kiel defaŭlta datumbazo
+ Datumbazo
+ Elekti por kopii %1$s al tondejo
+ Fermi kampojn
+ Forigi
+ Forigi kampon
+ Aldoni kampon
+ Pasvorta longeco
+ Pasvorta generilo
+ Forĵeti
+ Ĉu forĵeti ŝanĝojn\?
+ Validigi
+ Elementa piktogramo
+ Identigila informo
+ Dosiera informo
+ Aldoni eron
+ Aldoni grupon
+ Aldoni elementon
+ Aldoni nodon
+ Malfermi dosieron
+ Fono
+ Tondeja tempolimo
+ Ne povis purigi tondejon
+ Tondeja eraro
+ Tondejo purigita
+ Permesi
+ Krampoj
+ Aplikaĵo
+ Tempolimo de aplikaĵo
+ Ĉifrada algoritmo
+ Ĉifrado
+ Sekureco
+ Mastra ŝlosilo
+ Aldoni grupon
+ Redakti elementon
+ Aldoni elementon
+ Akcepti
+ Hejmpaĝo
+ Kontribuo
+ Kontakto
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 2bfed9ce2..08fa29360 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -547,4 +547,16 @@
Mostrar UUID
No es posible reconstruir adecuadamente la lista.
La URI de la base de datos no puede ser recuperada.
+ Intenta mostrar sugerencias de autocompletado directamente desde un teclado compatible
+ Añadidas sugerencias de autocompletado.
+ Acceso al archivo revocado por el administrador de archivos, cierra la base de datos y vuelva a abrirla desde su ubicación.
+ GiB
+ MiB
+ KiB
+ B
+ Sugerencias en línea
+ Sobrescribir las modificaciones externas guardando la base de datos o recargándola con los últimos cambios.
+ La información contenida en su archivo de base de datos ha sido modificada fuera de la aplicación.
+ Recargar la base de datos
+ El tipo de OTP existente no es reconocido por este formulario, su validación ya no puede generar correctamente el token.
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 0275a145c..788f82b7b 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -281,13 +281,14 @@
Thème de l’application
Thème utilisé dans l’application
- - Thème Jour
- - Thème Nuit
- - Thème Noir
- - Thème Foncé Classique
- - Thème Ciel et Océan
- - Thème Rouge Volcan
- - Thème Pro Violet
+ - Jour
+ - Nuit
+ - Noir
+ - Foncé Classique
+ - Ciel et Océan
+ - Rouge Volcan
+ - Pourpre Lumineux
+ - Pourpre Obscur
Collection d’icônes
Collection d’icônes utilisées dans l’application
@@ -561,4 +562,10 @@
Accès au dossier révoqué par le gestionnaire de fichiers, fermer la base de données et la rouvrir à partir de son emplacement.
Les informations contenues dans votre fichier de base de données ont été modifiées en dehors de l\'application.
Recharger la base de données
+ Gibioctets
+ Mébioctets
+ Kibioctets
+ Octets
+ Le type OTP existant n\'est pas reconnu par ce formulaire, sa validation peut ne plus générer correctement le jeton.
+ Annulé !
\ No newline at end of file
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index ada9f8ff9..9a13a8aed 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -75,13 +75,13 @@
Spremi
Naslov
Postavi jednokratnu lozinku
- Tip OTP-a
+ Vrsta jednokratne lozinke
Tajna
Razdoblje (u sekundama)
Brojač
Znamenke
Algoritam
- OTP
+ Jednokratna lozinka
URL
Korisničko ime
Odaberi datoteku ključa.
@@ -234,7 +234,7 @@
Nije moguće stvoriti datoteku
Nije moguće čitati bazu podataka.
Provjeri putanju do datoteke.
- Neispravan OTP tajni ključ.
+ Neispravna tajna jednokratne lozinke.
Upiši ime.
Nema dovoljno memorije za učitavanje cijele baze podataka.
Nije moguće učitati bazu podataka.
@@ -416,7 +416,7 @@
Paket ikona
Doprinesi
Zahvaljujemo na doprinosu.
- Postavi OTP
+ Postavi jednokratnu lozinku
Dodaj element
Kupnjom <strong>pro</strong> verzije,
Prikaži polja unosa u Magikeyboardu prilikom prikaza unosa
@@ -536,4 +536,9 @@
Prepiši vanjske promjene spremanjem baze podataka ili je ponovo učitaj s najnovijim promjenama.
Podaci u datoteci tvoje baze podataka izmijenjeni su izvan programa.
Ponovo učitaj bazu podataka
+ Ovaj obrazac ne prepoznaje postojeću vrstu jednokratne lozinke. Provjera valjanosti možda više neće pravilno generirati token.
+ GiB
+ MiB
+ KiB
+ B
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9f5b5db4d..2eaa5d595 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -362,7 +362,7 @@
Sblocco avanzato
Cronologia
Imposta password usa e getta
- Tipo di password usa e getta
+ Tipo di password OTP
Segreto
Periodo (secondi)
Contatore
@@ -555,4 +555,5 @@
Sovrascrivi le modifiche esterne salvano il database o ricaricalo con gli ultimi cambiamenti.
I dati nel tuo database sono stati modificati al di fuori di questa app.
Ricarica database
+ Il tipo di OTP esistente non è riconosciuto da questo modulo, la sua convalida potrebbe non generare più correttamente il token.
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 035412598..61bb0db6b 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -499,7 +499,8 @@
- Classic Dark
- Sky and Ocean
- Red Volcano
- - Purple Pro
+ - Purple Light
+ - Purple Dark
アイコンパック
アプリで使用するアイコンパック
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index d9a4a2507..b34a5b7c0 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -425,4 +425,20 @@
Søkemodus
Feltnavnet finnes allerede.
Legg til element
+ Avansert databaseopplåsing
+ Argon2id
+ Argon2d
+ B
+ Viser UUID-en tilhørende en oppføring
+ Vis UUID
+ GiB
+ MiB
+ KiB
+ Last opp
+ Legg til vedlegg
+ Lagre søkeinfo
+ Lukk database
+ Velg oppføring
+ Tilbake til forrige tastatur
+ Egendefinerte felter
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 309e9c809..2eb29a0a8 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -552,4 +552,9 @@
Nadpisz zewnętrzne modyfikacje, zapisując bazę danych lub przeładuj ją z najnowszymi zmianami.
Informacje zawarte w pliku bazy danych zostały zmodyfikowane poza aplikacją.
Załaduj ponownie bazę danych
+ GiB
+ MiB
+ KiB
+ B
+ Istniejący typ OTP nie jest rozpoznawany przez ten formularz, jego walidacja może już nie generować poprawnie tokenu.
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index c9b312fc9..95df8a771 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -552,4 +552,10 @@
Сохранить базу, перезаписав внешние изменения, или перезагрузить её с последними изменениями.
Информация, содержащаяся в файле базы, была изменена вне этого приложения.
Перезагрузить базу
+ Существующий тип OTP не распознаётся данной формой, его проверка больше не может корректно генерировать токен.
+ ГиБ
+ МиБ
+ КиБ
+ Б
+ Отменено!
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index f72af2d2a..51f38b2de 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -406,7 +406,7 @@
İndir %1$s
Başlatılıyor…
Devam ediyor: %1$d%%
- Sonlandırılıyor…
+ Sonuçlandırılıyor…
Tamamlandı!
Süresi dolmuş girdileri gizle
Süresi dolmuş girdiler gösterilmez
@@ -536,4 +536,10 @@
Veri tabanını kaydederek veya en son değişikliklerle yeniden yükleyerek harici değişikliklerin üzerine yazın.
Veri tabanı dosyanızda bulunan bilgiler, uygulamanın dışında değiştirildi.
Veri tabanını yeniden yükle
+ GiB
+ MiB
+ KiB
+ B
+ Mevcut OTP türü bu form tarafından tanınmıyor, doğrulaması artık belirteci doğru şekilde oluşturmayabilir.
+ İptal edildi!
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index e8c80deca..8bcbe1e43 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -552,4 +552,10 @@
Перезаписати зовнішні зміни, зберігши базу даних або перезавантажте її з найновішими змінами.
Відомості, що містяться у файлі бази даних, змінено за межами застосунку.
Перезавантажити базу даних
+ ГіБ
+ МіБ
+ КіБ
+ Б
+ Ця форма не розпізнає наявний тип OTP, його перевірка може надалі створювати не дійсний токен.
+ Скасовано!
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index f6d58bb79..cc6ecff56 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -552,4 +552,10 @@
通过保存数据库或用最新的更改重新加载数据库来覆盖外部修改。
数据库文件中包含的信息已在应用程序之外被修改。
重新加载数据库
+ GiB
+ 兆字节
+ 千位字节
+ 字节
+ 现有的 OTP 类型未被此表单所识别,其验证可能不再正确生成令牌。
+ 已取消!
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 8414e6d24..73f6b73cf 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -19,15 +19,14 @@
-->
+
-
-
+
-
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 8860d6fdf..f313fa6ef 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -20,11 +20,15 @@
#00ffffff
#ffffff
+ #E0FFFFFF
#000000
#0A0A0A
+ #101010
#0e0e0e
+ #E00E0E0E
#bdbdbd
#9e9e9e
+ #E0424242
#ffa726
#fb8c00
@@ -55,12 +59,17 @@
#9048bc
#743998
#653383
+ #4E1C6B
+ #451D5D
#FAFAFA
#303030
#000000
#F2EDFA
#F7F3FD
+ #E0F7F3FD
+ #361748
+ #E0361748
#555555
#000000
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index 299a25928..a0713ede3 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -332,6 +332,7 @@
KeepassDXStyle_Blue
KeepassDXStyle_Red
KeepassDXStyle_Purple
+ KeepassDXStyle_Purple_Dark
- @string/list_style_name_light
- @string/list_style_name_night
@@ -340,6 +341,7 @@
- @string/list_style_name_blue
- @string/list_style_name_red
- @string/list_style_name_purple
+ - @string/list_style_name_purple_dark
@string/material_resource_id
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3aaa72ef0..d1d3f2944 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -514,6 +514,7 @@
In progress: %1$d%%
Finalizing…
Complete!
+ Canceled!
Rijndael (AES)
Twofish
ChaCha20
@@ -563,7 +564,8 @@
- Classic Dark
- Sky and Ocean
- Red Volcano
- - Purple Pro
+ - Purple Light
+ - Purple Dark
Icon pack
Icon pack used in the app
diff --git a/app/src/main/res/values/style_black.xml b/app/src/main/res/values/style_black.xml
index af1813020..3b20887df 100644
--- a/app/src/main/res/values/style_black.xml
+++ b/app/src/main/res/values/style_black.xml
@@ -30,12 +30,14 @@
- @color/green_light
- @color/background_dark
- @style/KeepassDXStyle.Toolbar.Black
+ - @style/KeepassDXStyle.Toolbar.Popup.Black
- @style/KeepassDXStyle.Toolbar.Home.Black
- @style/KeepassDXStyle.Toolbar.Special.Black
- @style/KeepassDXStyle.Black.Dialog
- @style/KeepassDXStyle.Black.Dialog
- @style/KeepassDXStyle.ActionMode.Black
- @style/KeepassDXStyle.Cardview.Black
+ - @color/dark_transparent
+
+
@@ -62,4 +64,9 @@
- @color/blue_dark
- @color/blue_dark
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/style_dark.xml b/app/src/main/res/values/style_dark.xml
index 55c1b1f2c..a5c8a261e 100644
--- a/app/src/main/res/values/style_dark.xml
+++ b/app/src/main/res/values/style_dark.xml
@@ -35,6 +35,7 @@
- @style/KeepassDXStyle.Dark.Dialog
- @style/KeepassDXStyle.ActionMode.Dark
- @style/KeepassDXStyle.Cardview.Dark
+ - @color/dark_transparent
+
+
diff --git a/app/src/main/res/values/style_purple_dark.xml b/app/src/main/res/values/style_purple_dark.xml
new file mode 100644
index 000000000..abaed3b33
--- /dev/null
+++ b/app/src/main/res/values/style_purple_dark.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/style_red.xml b/app/src/main/res/values/style_red.xml
index 200bcf651..778540c4a 100644
--- a/app/src/main/res/values/style_red.xml
+++ b/app/src/main/res/values/style_red.xml
@@ -24,13 +24,15 @@
- @color/red_dark
- @color/orange
- @color/orange_light
- - @color/red
+ - @color/orange
- @color/red
- @color/red_lighter
- @color/background_night
- @style/KeepassDXStyle.Toolbar.Red
- @style/KeepassDXStyle.Toolbar.Home.Red
- @style/KeepassDXStyle.Toolbar.Special.Red
+ - @style/KeepassDXStyle.Red.Dialog
+ - @style/KeepassDXStyle.Red.Dialog
- @style/KeepassDXStyle.ActionMode.Red
@@ -57,4 +59,9 @@
- @color/orange
- @color/orange
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index ca871ad06..7aec9fbd8 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -55,6 +55,7 @@
- @style/KeepassDXStyle.TextAppearance
- @color/background_light
+ - #DDFFFFFF
- @color/text_color_light
- @color/colorText
@@ -74,7 +75,7 @@
- @style/KeepassDXStyle.Toolbar.Light
- - @style/KeepassDXStyle.Light.Toolbar.Popup
+ - @style/KeepassDXStyle.Toolbar.Popup.Light
- @style/KeepassDXStyle.Toolbar.Home.Light
- @style/KeepassDXStyle.Toolbar.Special.Light
- @style/KeepassDXStyle.Toolbar.Action
@@ -112,6 +113,7 @@
- @style/KeepassDXStyle.TextAppearance
- @color/background_night
+ - @color/grey_dark_transparent
- @color/text_color_night
- @color/colorTextInverse
@@ -131,7 +133,7 @@
- @style/KeepassDXStyle.Toolbar.Night
- - @style/KeepassDXStyle.Night.Toolbar.Popup
+ - @style/KeepassDXStyle.Toolbar.Popup.Night
- @style/KeepassDXStyle.Toolbar.Home.Night
- @style/KeepassDXStyle.Toolbar.Special.Night
- @style/KeepassDXStyle.Toolbar.Action
@@ -146,17 +148,11 @@
-
+
-
+