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