Merge branch 'feature/Image_Viewer' into develop #473

This commit is contained in:
J-Jamet
2021-02-10 11:49:41 +01:00
42 changed files with 2228 additions and 420 deletions

View File

@@ -17,7 +17,7 @@ android {
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" ]
@@ -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'
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

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

Specials
<20>

View File

@@ -0,0 +1,156 @@
package com.kunzisoft.keepass.tests.stream
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import junit.framework.TestCase.assertEquals
import org.junit.Test
import java.io.DataInputStream
import java.io.File
import java.io.InputStream
import java.lang.Exception
import java.security.MessageDigest
class BinaryAttachmentTest {
private val context: Context by lazy {
InstrumentationRegistry.getInstrumentation().context
}
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
private val loadedKey = Database.LoadedKey.generateNewCipherKey()
private fun saveBinary(asset: String, binaryAttachment: BinaryAttachment) {
context.assets.open(asset).use { assetInputStream ->
binaryAttachment.getOutputDataStream(loadedKey).use { binaryOutputStream ->
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
binaryOutputStream.write(buffer)
}
}
}
}
@Test
fun testSaveTextInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
assertEquals("Save text binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save text binary MD5 failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testSaveImageInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
assertEquals("Save image binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save image binary failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testCompressText() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(fileC)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
saveBinary(TEST_TEXT_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress text length failed.", binaryA.length, binaryB.length)
assertEquals("Compress text MD5 failed.", binaryA.md5(), binaryB.md5())
binaryB.decompress(loadedKey)
assertEquals("Decompress text length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress text MD5 failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testCompressImage() {
val binaryA = BinaryAttachment(fileA)
var binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(fileC)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
saveBinary(TEST_IMAGE_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress image length failed.", binaryA.length, binaryA.length)
assertEquals("Compress image failed.", binaryA.md5(), binaryA.md5())
binaryB = BinaryAttachment(fileB, true)
binaryB.decompress(loadedKey)
assertEquals("Decompress image length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress image failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testReadText() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_TEXT_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
@Test
fun testReadImage() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_IMAGE_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
private fun streamAreEquals(inputStreamA: InputStream,
inputStreamB: InputStream): Boolean {
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
val dataInputStreamB = DataInputStream(inputStreamB)
try {
var len: Int
while (inputStreamA.read(bufferA).also { len = it } > 0) {
dataInputStreamB.readFully(bufferB, 0, len)
for (i in 0 until len) {
if (bufferA[i] != bufferB[i])
return false
}
}
return inputStreamB.read() < 0 // is the end of the second file also.
} catch (e: Exception) {
return false
}
finally {
inputStreamA.close()
inputStreamB.close()
}
}
private fun BinaryAttachment.md5(): String {
val md = MessageDigest.getInstance("MD5")
return this.getInputDataStream(loadedKey).use { fis ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
generateSequence {
when (val bytesRead = fis.read(buffer)) {
-1 -> null
else -> bytesRead
}
}.forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
md.digest().joinToString("") { "%02x".format(it) }
}
}
companion object {
private const val TEST_FILE_CACHE_A = "testA"
private const val TEST_FILE_CACHE_B = "testB"
private const val TEST_FILE_CACHE_C = "testC"
private const val TEST_IMAGE_ASSET = "test_image.png"
private const val TEST_TEXT_ASSET = "test_text.txt"
}
}

View File

@@ -129,6 +129,9 @@
<activity
android:name="com.kunzisoft.keepass.activities.EntryActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:windowSoftInputMode="adjustResize" />

File diff suppressed because it is too large Load Diff

View File

@@ -126,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)

View File

@@ -106,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<Attachment>()
private var mTempAttachments = ArrayList<EntryAttachmentState>()
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -198,7 +198,7 @@ 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)
@@ -354,25 +354,20 @@ 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())
}
}
@@ -472,10 +467,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)
}
}
@@ -539,14 +536,33 @@ 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.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)
}
}
}

View File

@@ -41,6 +41,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
@@ -86,6 +87,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
@@ -127,6 +129,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)
@@ -502,9 +505,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 +535,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 +543,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)
}
}
}

View File

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

View File

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

View File

@@ -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<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(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)
@@ -114,7 +151,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
}
}
}
holder.itemView.setOnClickListener(null)
holder.binaryFileInfo.setOnClickListener(null)
}
StreamDirection.DOWNLOAD -> {
holder.binaryFileProgressIcon.isActivated = false
@@ -127,7 +164,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
}
progress = entryAttachmentState.downloadProgression
}
holder.itemView.setOnClickListener {
holder.binaryFileInfo.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState)
}
}
@@ -136,6 +173,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)

View File

@@ -57,7 +57,7 @@ class NodeAdapter (private val context: Context)
private val mNodeSortedList: SortedList<Node>
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

View File

@@ -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<Attachment?> {
return arrayOfNulls(size)
}
fun loadBitmap(attachment: Attachment,
binaryCipherKey: Database.LoadedKey?,
actionOnFinish: (Bitmap?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<Bitmap?> = async {
runCatching {
binaryCipherKey?.let { binaryKey ->
var bitmap: Bitmap?
attachment.binaryAttachment.getUnGzipInputDataStream(binaryKey).use { bitmapInputStream ->
bitmap = BitmapFactory.decodeStream(bitmapInputStream)
}
bitmap
}
}.getOrNull()
}
withContext(Dispatchers.Main) {
actionOnFinish(asyncResult.await())
}
}
}
}
}
}

View File

@@ -47,7 +47,11 @@ 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
@@ -75,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()
@@ -321,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,
@@ -404,6 +435,7 @@ class Database {
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
LoadedKey.generateNewCipherKey(),
progressTaskUpdater,
fixDuplicateUUID)
},
@@ -412,6 +444,7 @@ class Database {
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
LoadedKey.generateNewCipherKey(),
progressTaskUpdater,
fixDuplicateUUID)
}

View File

@@ -346,12 +346,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|| entryKDBX?.containsAttachment() == true
}
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
attachments.forEach {
putAttachment(it, binaryPool)
}
}
private fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment)
@@ -467,7 +461,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments)
newEntryInfo.attachments.forEach { attachment ->
putAttachment(attachment, binaryPool)
}
}
database?.stopManageEntry(this)

View File

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

View File

@@ -281,7 +281,5 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
const val BUFFER_SIZE_BYTES = 3 * 128
}
}

View File

@@ -213,8 +213,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
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)
}
@@ -224,7 +226,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
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)
}
@@ -709,7 +713,5 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP
const val BUFFER_SIZE_BYTES = 3 * 128
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.element.database
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"

View File

@@ -316,7 +316,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, 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
}

View File

@@ -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
@@ -40,7 +41,7 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
keyfileInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@@ -50,4 +51,20 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.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,16 +49,16 @@ import javax.crypto.spec.SecretKeySpec
class DatabaseInputKDB(cacheDirectory: File)
: DatabaseInput<DatabaseKDB>(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?,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@@ -67,7 +68,33 @@ class DatabaseInputKDB(cacheDirectory: File)
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.masterKey = masterKey
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
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) {
mDatabase.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey
}
}
@@ -101,38 +128,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 +172,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 +196,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 +219,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 +238,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 +253,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 +282,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 +327,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 +342,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 +365,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<GroupKDB>) {
@@ -375,18 +403,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
}

View File

@@ -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,11 +97,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
keyfileInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.retrieveMasterKey(password, keyInputStream)
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@@ -114,6 +115,31 @@ class DatabaseInputKDBX(cacheDirectory: File)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: Database.LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
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
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
@@ -273,8 +299,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 +1037,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 +1113,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
return xpp
}
}
}
@Throws(IOException::class, XmlPullParserException::class)

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.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()
}
}

View File

@@ -32,7 +32,6 @@ import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.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)))
}
}

View File

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

View File

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

View File

@@ -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>() ?: StreamDirection.DOWNLOAD,
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
parcel.readInt())
parcel.readInt(),
parcel.readEnum<AttachmentState>() ?: 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 {

View File

@@ -29,11 +29,12 @@ 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.stream.readAllBytes
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.*
@@ -345,7 +346,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to upload or download file", e)
e.printStackTrace()
progressResult = false
}
progressResult
@@ -372,17 +373,19 @@ class AttachmentFileNotificationService: LockNotificationService() {
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)
}
}
}
}
@@ -396,10 +399,10 @@ class AttachmentFileNotificationService: LockNotificationService() {
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) { buffer ->
outputStream.write(buffer)
dataUploaded += buffer.size
try {

View File

@@ -30,7 +30,8 @@ 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,
readBytes: (bytesRead: ByteArray) -> Unit) {
val buffer = ByteArray(bufferSize)
var read = 0
while (read != -1) {
@@ -50,7 +51,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)

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

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

View File

@@ -25,9 +25,8 @@
<attr name="toolbarPopupAppearance" format="reference" />
<attr name="colorAccentLight" format="reference|color" />
<attr name="cardBackgroundTransparentColor" format="reference|color" />
<attr name="iconPreferenceColor" format="reference|color" />
<attr name="textColorInverse" format="reference|color" />
<declare-styleable name="explanationDialog">

View File

@@ -20,11 +20,14 @@
<resources>
<color name="transparent">#00ffffff</color>
<color name="white">#ffffff</color>
<color name="white_transparent">#E0FFFFFF</color>
<color name="black">#000000</color>
<color name="darker">#0A0A0A</color>
<color name="dark">#0e0e0e</color>
<color name="dark_transparent">#E00E0E0E</color>
<color name="grey">#bdbdbd</color>
<color name="grey_dark">#9e9e9e</color>
<color name="grey_dark_transparent">#E0424242</color>
<color name="orange_light">#ffa726</color>
<color name="orange">#fb8c00</color>
@@ -61,6 +64,7 @@
<color name="background_dark">#000000</color>
<color name="background_purple">#F2EDFA</color>
<color name="background_purple_light">#F7F3FD</color>
<color name="background_purple_light_transparent">#E0F7F3FD</color>
<color name="icon_background">#555555</color>
<color name="icon_text">#000000</color>

View File

@@ -36,6 +36,7 @@
<item name="alertDialogTheme">@style/KeepassDXStyle.Black.Dialog</item>
<item name="actionModeStyle">@style/KeepassDXStyle.ActionMode.Black</item>
<item name="cardViewStyle">@style/KeepassDXStyle.Cardview.Black</item>
<item name="cardBackgroundTransparentColor">@color/dark_transparent</item>
</style>
<!-- Toolbar Style Black -->
<style name="KeepassDXStyle.Toolbar.Black" parent="KeepassDXStyle.Black">

View File

@@ -35,6 +35,7 @@
<item name="alertDialogTheme">@style/KeepassDXStyle.Dark.Dialog</item>
<item name="actionModeStyle">@style/KeepassDXStyle.ActionMode.Dark</item>
<item name="cardViewStyle">@style/KeepassDXStyle.Cardview.Dark</item>
<item name="cardBackgroundTransparentColor">@color/dark_transparent</item>
</style>
<!-- Toolbar Style Dark -->
<style name="KeepassDXStyle.Toolbar.Dark" parent="KeepassDXStyle.Dark">

View File

@@ -36,6 +36,7 @@
<item name="alertDialogTheme">@style/KeepassDXStyle.Purple.Dialog</item>
<item name="actionModeStyle">@style/KeepassDXStyle.ActionMode.Purple</item>
<item name="cardViewStyle">@style/KeepassDXStyle.Cardview.Purple</item>
<item name="cardBackgroundTransparentColor">@color/background_purple_light_transparent</item>
</style>
<!-- Toolbar Style Purple -->
<style name="KeepassDXStyle.Toolbar.Purple" parent="KeepassDXStyle.Purple">

View File

@@ -55,6 +55,7 @@
<item name="android:textAppearanceMedium">@style/KeepassDXStyle.TextAppearance</item>
<item name="android:windowBackground">@color/background_light</item>
<item name="cardBackgroundTransparentColor">#DDFFFFFF</item>
<item name="android:textColor">@color/text_color_light</item>
<item name="android:editTextColor">@color/colorText</item>
@@ -112,6 +113,7 @@
<item name="android:textAppearanceMedium">@style/KeepassDXStyle.TextAppearance</item>
<item name="android:windowBackground">@color/background_night</item>
<item name="cardBackgroundTransparentColor">@color/grey_dark_transparent</item>
<item name="android:textColor">@color/text_color_night</item>
<item name="android:editTextColor">@color/colorTextInverse</item>