diff --git a/CHANGELOG b/CHANGELOG index 47f1b0fcb..854659cef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +KeePassDX(2.9.13) + * Binary image viewer #473 #749 + * Fix TOTP plugin settings #878 + * Allow Emoji #796 + * Scroll and better UI in entry edition screen #876 + * Better UI #876 + * Fix themes and add Purple Dark #889 + * Allow OTP with many padding #585 + * Add notes in groups #734 + KeePassDX(2.9.12) * Fix OTP token type #863 * Fix auto open biometric prompt #862 diff --git a/app/build.gradle b/app/build.gradle index a9a280a45..34df6faf8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,12 +12,12 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 30 - versionCode = 56 - versionName = "2.9.12" + versionCode = 57 + versionName = "2.9.13" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" - testInstrumentationRunner = "android.test.InstrumentationTestRunner" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}" manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ] @@ -51,7 +51,7 @@ android { buildConfigField "String", "BUILD_VERSION", "\"libre\"" buildConfigField "boolean", "FULL_VERSION", "true" buildConfigField "boolean", "CLOSED_STORE", "false" - buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}" + buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\",\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" } pro { @@ -70,7 +70,7 @@ android { buildConfigField "String", "BUILD_VERSION", "\"free\"" buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "CLOSED_STORE", "true" - buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}" + buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\",\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ] } @@ -82,6 +82,10 @@ android { free.res.srcDir 'src/free/res' } + testOptions { + unitTests.includeAndroidResources = true + } + compileOptions { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 @@ -120,14 +124,15 @@ dependencies { implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4' // Education implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' - // Apache Commons Collections + // Apache Commons implementation 'commons-collections:commons-collections:3.2.2' - // Apache Commons Codec + implementation 'commons-io:commons-io:2.8.0' implementation 'commons-codec:commons-codec:1.15' // Icon pack implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-material') // Tests - androidTestImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' } diff --git a/app/src/androidTest/assets/test_image.png b/app/src/androidTest/assets/test_image.png new file mode 100644 index 000000000..ec28a755c Binary files /dev/null and b/app/src/androidTest/assets/test_image.png differ diff --git a/app/src/androidTest/assets/test_text.txt b/app/src/androidTest/assets/test_text.txt new file mode 100644 index 000000000..cc66555ac --- /dev/null +++ b/app/src/androidTest/assets/test_text.txt @@ -0,0 +1,133 @@ +Basic Latin + ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ +Latin-1 Supplement + ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ­ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ +Latin Extended-A + Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ +Latin Extended-B + ƀ Ɓ Ƃ ƃ Ƅ ƅ Ɔ Ƈ ƈ Ɖ Ɗ Ƌ ƌ ƍ Ǝ Ə Ɛ Ƒ ƒ Ɠ Ɣ ƕ Ɩ Ɨ Ƙ ƙ ƚ ƛ Ɯ Ɲ ƞ Ɵ Ơ ơ Ƣ ƣ Ƥ ƥ Ʀ Ƨ ƨ Ʃ ƪ ƫ Ƭ ƭ Ʈ Ư ư Ʊ Ʋ Ƴ ƴ Ƶ ƶ Ʒ Ƹ ƹ ƺ ƻ Ƽ ƽ ƾ ƿ ǀ ǁ ǂ ǃ DŽ Dž dž LJ Lj lj NJ Nj nj Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ Ǘ ǘ Ǚ ǚ Ǜ ǜ ǝ Ǟ ǟ Ǡ ǡ Ǣ ǣ Ǥ ǥ Ǧ ǧ Ǩ ǩ Ǫ ǫ Ǭ ǭ Ǯ ǯ ǰ DZ Dz dz Ǵ ǵ Ǻ ǻ Ǽ ǽ Ǿ ǿ Ȁ ȁ Ȃ ȃ ... +IPA Extensions + ɐ ɑ ɒ ɓ ɔ ɕ ɖ ɗ ɘ ə ɚ ɛ ɜ ɝ ɞ ɟ ɠ ɡ ɢ ɣ ɤ ɥ ɦ ɧ ɨ ɩ ɪ ɫ ɬ ɭ ɮ ɯ ɰ ɱ ɲ ɳ ɴ ɵ ɶ ɷ ɸ ɹ ɺ ɻ ɼ ɽ ɾ ɿ ʀ ʁ ʂ ʃ ʄ ʅ ʆ ʇ ʈ ʉ ʊ ʋ ʌ ʍ ʎ ʏ ʐ ʑ ʒ ʓ ʔ ʕ ʖ ʗ ʘ ʙ ʚ ʛ ʜ ʝ ʞ ʟ ʠ ʡ ʢ ʣ ʤ ʥ ʦ ʧ ʨ +Spacing Modifier Letters + ʰ ʱ ʲ ʳ ʴ ʵ ʶ ʷ ʸ ʹ ʺ ʻ ʼ ʽ ʾ ʿ ˀ ˁ ˂ ˃ ˄ ˅ ˆ ˇ ˈ ˉ ˊ ˋ ˌ ˍ ˎ ˏ ː ˑ ˒ ˓ ˔ ˕ ˖ ˗ ˘ ˙ ˚ ˛ ˜ ˝ ˞ ˠ ˡ ˢ ˣ ˤ ˥ ˦ ˧ ˨ ˩ +Combining Diacritical Marks + ̀ ́ ̂ ̃ ̄ ̅ ̆ ̇ ̈ ̉ ̊ ̋ ̌ ̍ ̎ ̏ ̐ ̑ ̒ ̓ ̔ ̕ ̖ ̗ ̘ ̙ ̚ ̛ ̜ ̝ ̞ ̟ ̠ ̡ ̢ ̣ ̤ ̥ ̦ ̧ ̨ ̩ ̪ ̫ ̬ ̭ ̮ ̯ ̰ ̱ ̲ ̳ ̴ ̵ ̶ ̷ ̸ ̹ ̺ ̻ ̼ ̽ ̾ ̿ ̀ ́ ͂ ̓ ̈́ ͅ ͠ ͡ +Greek + ʹ ͵ ͺ ; ΄ ΅ Ά · Έ Ή Ί Ό Ύ Ώ ΐ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή ί ΰ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω ϊ ϋ ό ύ ώ ϐ ϑ ϒ ϓ ϔ ϕ ϖ Ϛ Ϝ Ϟ Ϡ Ϣ ϣ Ϥ ϥ Ϧ ϧ Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ +Cyrillic + Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ё ђ ѓ є ѕ і ї ј љ њ ћ ќ ў џ Ѡ ѡ Ѣ ѣ Ѥ ѥ Ѧ ѧ Ѩ ѩ Ѫ ѫ Ѭ ѭ Ѯ ѯ Ѱ ѱ Ѳ ѳ Ѵ ѵ Ѷ ѷ Ѹ ѹ Ѻ ѻ Ѽ ѽ Ѿ ѿ Ҁ ҁ ҂ ҃ ... +Armenian + Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք Օ Ֆ ՙ ՚ ՛ ՜ ՝ ՞ ՟ ա բ գ դ ե զ է ը թ ժ ի լ խ ծ կ հ ձ ղ ճ մ յ ն շ ո չ պ ջ ռ ս վ տ ր ց ւ փ ք օ ֆ և ։ +Hebrew + ֑ ֒ ֓ ֔ ֕ ֖ ֗ ֘ ֙ ֚ ֛ ֜ ֝ ֞ ֟ ֠ ֡ ֣ ֤ ֥ ֦ ֧ ֨ ֩ ֪ ֫ ֬ ֭ ֮ ֯ ְ ֱ ֲ ֳ ִ ֵ ֶ ַ ָ ֹ ֻ ּ ֽ ־ ֿ ׀ ׁ ׂ ׃ ׄ א ב ג ד ה ו ז ח ט י ך כ ל ם מ ן נ ס ע ף פ ץ צ ק ר ש ת װ ױ ײ ׳ ״ +Arabic + ، ؛ ؟ ء آ أ ؤ إ ئ ا ب ة ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ـ ف ق ك ل م ن ه و ى ي ً ٌ ٍ َ ُ ِ ّ ْ ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٪ ٫ ٬ ٭ ٰ ٱ ٲ ٳ ٴ ٵ ٶ ٷ ٸ ٹ ٺ ٻ ټ ٽ پ ٿ ڀ ځ ڂ ڃ ڄ څ چ ڇ ڈ ډ ڊ ڋ ڌ ڍ ڎ ڏ ڐ ڑ ڒ ړ ڔ ڕ ږ ڗ ژ ڙ ښ ڛ ڜ ڝ ڞ ڟ ڠ ڡ ڢ ڣ ڤ ڥ ڦ ڧ ڨ ک ڪ ګ ڬ ڭ ڮ گ ڰ ڱ ... +Devanagari + ँ ं ः अ आ इ ई उ ऊ ऋ ऌ ऍ ऎ ए ऐ ऑ ऒ ओ औ क ख ग घ ङ च छ ज झ ञ ट ठ ड ढ ण त थ द ध न ऩ प फ ब भ म य र ऱ ल ळ ऴ व श ष स ह ़ ऽ ा ि ी ु ू ृ ॄ ॅ ॆ े ै ॉ ॊ ो ौ ् ॐ ॑ ॒ ॓ ॔ क़ ख़ ग़ ज़ ड़ ढ़ फ़ य़ ॠ ॡ ॢ ॣ । ॥ ० १ २ ३ ४ ५ ६ ७ ८ ९ ॰ +Bengali + ঁ ং ঃ অ আ ই ঈ উ ঊ ঋ ঌ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ় া ি ী ু ূ ৃ ৄ ে ৈ ো ৌ ্ ৗ ড় ঢ় য় ৠ ৡ ৢ ৣ ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ ৰ ৱ ৲ ৳ ৴ ৵ ৶ ৷ ৸ ৹ ৺ +Gurmukhi + ਂ ਅ ਆ ਇ ਈ ਉ ਊ ਏ ਐ ਓ ਔ ਕ ਖ ਗ ਘ ਙ ਚ ਛ ਜ ਝ ਞ ਟ ਠ ਡ ਢ ਣ ਤ ਥ ਦ ਧ ਨ ਪ ਫ ਬ ਭ ਮ ਯ ਰ ਲ ਲ਼ ਵ ਸ਼ ਸ ਹ ਼ ਾ ਿ ੀ ੁ ੂ ੇ ੈ ੋ ੌ ੍ ਖ਼ ਗ਼ ਜ਼ ੜ ਫ਼ ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯ ੰ ੱ ੲ ੳ ੴ +Gujarati + ઁ ં ઃ અ આ ઇ ઈ ઉ ઊ ઋ ઍ એ ઐ ઑ ઓ ઔ ક ખ ગ ઘ ઙ ચ છ જ ઝ ઞ ટ ઠ ડ ઢ ણ ત થ દ ધ ન પ ફ બ ભ મ ય ર લ ળ વ શ ષ સ હ ઼ ઽ ા િ ી ુ ૂ ૃ ૄ ૅ ે ૈ ૉ ો ૌ ્ ૐ ૠ ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯ +Oriya + ଁ ଂ ଃ ଅ ଆ ଇ ଈ ଉ ଊ ଋ ଌ ଏ ଐ ଓ ଔ କ ଖ ଗ ଘ ଙ ଚ ଛ ଜ ଝ ଞ ଟ ଠ ଡ ଢ ଣ ତ ଥ ଦ ଧ ନ ପ ଫ ବ ଭ ମ ଯ ର ଲ ଳ ଶ ଷ ସ ହ ଼ ଽ ା ି ୀ ୁ ୂ ୃ େ ୈ ୋ ୌ ୍ ୖ ୗ ଡ଼ ଢ଼ ୟ ୠ ୡ ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯ ୰ +Tamil + ஂ ஃ அ ஆ இ ஈ உ ஊ எ ஏ ஐ ஒ ஓ ஔ க ங ச ஜ ஞ ட ண த ந ன ப ம ய ர ற ல ள ழ வ ஷ ஸ ஹ ா ி ீ ு ூ ெ ே ை ொ ோ ௌ ் ௗ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ ௰ ௱ ௲ +Telugu + ఁ ం ః అ ఆ ఇ ఈ ఉ ఊ ఋ ఌ ఎ ఏ ఐ ఒ ఓ ఔ క ఖ గ ఘ ఙ చ ఛ జ ఝ ఞ ట ఠ డ ఢ ణ త థ ద ధ న ప ఫ బ భ మ య ర ఱ ల ళ వ శ ష స హ ా ి ీ ు ూ ృ ౄ ె ే ై ొ ో ౌ ్ ౕ ౖ ౠ ౡ ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯ +Kannada + ಂ ಃ ಅ ಆ ಇ ಈ ಉ ಊ ಋ ಌ ಎ ಏ ಐ ಒ ಓ ಔ ಕ ಖ ಗ ಘ ಙ ಚ ಛ ಜ ಝ ಞ ಟ ಠ ಡ ಢ ಣ ತ ಥ ದ ಧ ನ ಪ ಫ ಬ ಭ ಮ ಯ ರ ಱ ಲ ಳ ವ ಶ ಷ ಸ ಹ ಾ ಿ ೀ ು ೂ ೃ ೄ ೆ ೇ ೈ ೊ ೋ ೌ ್ ೕ ೖ ೞ ೠ ೡ ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯ +Malayalam + ം ഃ അ ആ ഇ ഈ ഉ ഊ ഋ ഌ എ ഏ ഐ ഒ ഓ ഔ ക ഖ ഗ ഘ ങ ച ഛ ജ ഝ ഞ ട ഠ ഡ ഢ ണ ത ഥ ദ ധ ന പ ഫ ബ ഭ മ യ ര റ ല ള ഴ വ ശ ഷ സ ഹ ാ ി ീ ു ൂ ൃ െ േ ൈ ൊ ോ ൌ ് ൗ ൠ ൡ ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯ +Thai + ก ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ ญ ฎ ฏ ฐ ฑ ฒ ณ ด ต ถ ท ธ น บ ป ผ ฝ พ ฟ ภ ม ย ร ฤ ล ฦ ว ศ ษ ส ห ฬ อ ฮ ฯ ะ ั า ำ ิ ี ึ ื ุ ู ฺ ฿ เ แ โ ใ ไ ๅ ๆ ็ ่ ้ ๊ ๋ ์ ํ ๎ ๏ ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๚ ๛ +Lao + ກ ຂ ຄ ງ ຈ ຊ ຍ ດ ຕ ຖ ທ ນ ບ ປ ຜ ຝ ພ ຟ ມ ຢ ຣ ລ ວ ສ ຫ ອ ຮ ຯ ະ ັ າ ຳ ິ ີ ຶ ື ຸ ູ ົ ຼ ຽ ເ ແ ໂ ໃ ໄ ໆ ່ ້ ໊ ໋ ໌ ໍ ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ ໜ ໝ +Tibetan + ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊ ་ ༌ ། ༎ ༏ ༐ ༑ ༒ ༓ ༔ ༕ ༖ ༗ ༘ ༙ ༚ ༛ ༜ ༝ ༞ ༟ ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ ༪ ༫ ༬ ༭ ༮ ༯ ༰ ༱ ༲ ༳ ༴ ༵ ༶ ༷ ༸ ༹ ༺ ༻ ༼ ༽ ༾ ༿ ཀ ཁ ག གྷ ང ཅ ཆ ཇ ཉ ཊ ཋ ཌ ཌྷ ཎ ཏ ཐ ད དྷ ན པ ཕ བ བྷ མ ཙ ཚ ཛ ཛྷ ཝ ཞ ཟ འ ཡ ར ལ ཤ ཥ ས ཧ ཨ ཀྵ ཱ ི ཱི ུ ཱུ ྲྀ ཷ ླྀ ཹ ེ ཻ ོ ཽ ཾ ཿ ྀ ཱྀ ྂ ྃ ྄ ྅ ྆ ྇ ... +Georgian + Ⴀ Ⴁ Ⴂ Ⴃ Ⴄ Ⴅ Ⴆ Ⴇ Ⴈ Ⴉ Ⴊ Ⴋ Ⴌ Ⴍ Ⴎ Ⴏ Ⴐ Ⴑ Ⴒ Ⴓ Ⴔ Ⴕ Ⴖ Ⴗ Ⴘ Ⴙ Ⴚ Ⴛ Ⴜ Ⴝ Ⴞ Ⴟ Ⴠ Ⴡ Ⴢ Ⴣ Ⴤ Ⴥ ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ ჟ რ ს ტ უ ფ ქ ღ ყ შ ჩ ც ძ წ ჭ ხ ჯ ჰ ჱ ჲ ჳ ჴ ჵ ჶ ჻ +Hangul Jamo + ᄀ ᄁ ᄂ ᄃ ᄄ ᄅ ᄆ ᄇ ᄈ ᄉ ᄊ ᄋ ᄌ ᄍ ᄎ ᄏ ᄐ ᄑ ᄒ ᄓ ᄔ ᄕ ᄖ ᄗ ᄘ ᄙ ᄚ ᄛ ᄜ ᄝ ᄞ ᄟ ᄠ ᄡ ᄢ ᄣ ᄤ ᄥ ᄦ ᄧ ᄨ ᄩ ᄪ ᄫ ᄬ ᄭ ᄮ ᄯ ᄰ ᄱ ᄲ ᄳ ᄴ ᄵ ᄶ ᄷ ᄸ ᄹ ᄺ ᄻ ᄼ ᄽ ᄾ ᄿ ᅀ ᅁ ᅂ ᅃ ᅄ ᅅ ᅆ ᅇ ᅈ ᅉ ᅊ ᅋ ᅌ ᅍ ᅎ ᅏ ᅐ ᅑ ᅒ ᅓ ᅔ ᅕ ᅖ ᅗ ᅘ ᅙ ᅟ ᅠ ᅡ ᅢ ᅣ ᅤ ᅥ ᅦ ᅧ ᅨ ᅩ ᅪ ᅫ ᅬ ᅭ ᅮ ᅯ ᅰ ᅱ ᅲ ᅳ ᅴ ᅵ ᅶ ᅷ ᅸ ᅹ ᅺ ᅻ ᅼ ᅽ ᅾ ᅿ ᆀ ᆁ ᆂ ᆃ ᆄ ... +Latin Extended Additional + Ḁ ḁ Ḃ ḃ Ḅ ḅ Ḇ ḇ Ḉ ḉ Ḋ ḋ Ḍ ḍ Ḏ ḏ Ḑ ḑ Ḓ ḓ Ḕ ḕ Ḗ ḗ Ḙ ḙ Ḛ ḛ Ḝ ḝ Ḟ ḟ Ḡ ḡ Ḣ ḣ Ḥ ḥ Ḧ ḧ Ḩ ḩ Ḫ ḫ Ḭ ḭ Ḯ ḯ Ḱ ḱ Ḳ ḳ Ḵ ḵ Ḷ ḷ Ḹ ḹ Ḻ ḻ Ḽ ḽ Ḿ ḿ Ṁ ṁ Ṃ ṃ Ṅ ṅ Ṇ ṇ Ṉ ṉ Ṋ ṋ Ṍ ṍ Ṏ ṏ Ṑ ṑ Ṓ ṓ Ṕ ṕ Ṗ ṗ Ṙ ṙ Ṛ ṛ Ṝ ṝ Ṟ ṟ Ṡ ṡ Ṣ ṣ Ṥ ṥ Ṧ ṧ Ṩ ṩ Ṫ ṫ Ṭ ṭ Ṯ ṯ Ṱ ṱ Ṳ ṳ Ṵ ṵ Ṷ ṷ Ṹ ṹ Ṻ ṻ Ṽ ṽ Ṿ ṿ ... +Greek Extended + ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ... +General Punctuation +                       ​ ‌ ‍ ‎ ‏ ‐ ‑ ‒ – — ― ‖ ‗ ‘ ’ ‚ ‛ “ ” „ ‟ † ‡ • ‣ ․ ‥ … ‧ 
 
 ‪ ‫ ‬ ‭ ‮ ‰ ‱ ′ ″ ‴ ‵ ‶ ‷ ‸ ‹ › ※ ‼ ‽ ‾ ‿ ⁀ ⁁ ⁂ ⁃ ⁄ ⁅ ⁆       +Superscripts and Subscripts + ⁰ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎ +Currency Symbols + ₠ ₡ ₢ ₣ ₤ ₥ ₦ ₧ ₨ ₩ ₪ ₫ +Combining Marks for Symbols + ⃐ ⃑ ⃒ ⃓ ⃔ ⃕ ⃖ ⃗ ⃘ ⃙ ⃚ ⃛ ⃜ ⃝ ⃞ ⃟ ⃠ ⃡ +Letterlike Symbols + ℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟ ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ +Number Forms + ⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ⅟ Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ Ⅼ Ⅽ Ⅾ Ⅿ ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ ⅺ ⅻ ⅼ ⅽ ⅾ ⅿ ↀ ↁ ↂ +Arrows + ← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟ ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯ ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿ ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏ ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟ ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪ +Mathematical Operators + ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏ ∐ ∑ − ∓ ∔ ∕ ∖ ∗ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟ ∠ ∡ ∢ ∣ ∤ ∥ ∦ ∧ ∨ ∩ ∪ ∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ∴ ∵ ∶ ∷ ∸ ∹ ∺ ∻ ∼ ∽ ∾ ∿ ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ... +Miscellaneous Technical + ⌀ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏ ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟ ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯ ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿ ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏ ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟ ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯ ⍰ ⍱ ⍲ ⍳ ⍴ ⍵ ⍶ ⍷ ⍸ ⍹ ⍺ +Control Pictures + ␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ ␠ ␡ ␢ ␣ ␤ +Optical Character Recognition + ⑀ ⑁ ⑂ ⑃ ⑄ ⑅ ⑆ ⑇ ⑈ ⑉ ⑊ +Enclosed Alphanumerics + ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ... +Box Drawing + ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ +Block Elements + ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ +Geometric Shapes + ■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯ +Miscellaneous Symbols + ☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯ +Dingbats + ✁ ✂ ✃ ✄ ✆ ✇ ✈ ✉ ✌ ✍ ✎ ✏ ✐ ✑ ✒ ✓ ✔ ✕ ✖ ✗ ✘ ✙ ✚ ✛ ✜ ✝ ✞ ✟ ✠ ✡ ✢ ✣ ✤ ✥ ✦ ✧ ✩ ✪ ✫ ✬ ✭ ✮ ✯ ✰ ✱ ✲ ✳ ✴ ✵ ✶ ✷ ✸ ✹ ✺ ✻ ✼ ✽ ✾ ✿ ❀ ❁ ❂ ❃ ❄ ❅ ❆ ❇ ❈ ❉ ❊ ❋ ❍ ❏ ❐ ❑ ❒ ❖ ❘ ❙ ❚ ❛ ❜ ❝ ❞ ❡ ❢ ❣ ❤ ❥ ❦ ❧ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑ ➒ ➓ ➔ ➘ ➙ ➚ ➛ ➜ ➝ ... +CJK Symbols and Punctuation +   、 。 〃 〄 々 〆 〇 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〔 〕 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〳 〴 〵 〶 〷 〿 +Hiragana + ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゙ ゚ ゛ ゜ ゝ ゞ +Katakana + ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ノ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ ヷ ヸ ヹ ヺ ・ ー ヽ ヾ +Bopomofo + ㄅ ㄆ ㄇ ㄈ ㄉ ㄊ ㄋ ㄌ ㄍ ㄎ ㄏ ㄐ ㄑ ㄒ ㄓ ㄔ ㄕ ㄖ ㄗ ㄘ ㄙ ㄚ ㄛ ㄜ ㄝ ㄞ ㄟ ㄠ ㄡ ㄢ ㄣ ㄤ ㄥ ㄦ ㄧ ㄨ ㄩ ㄪ ㄫ ㄬ +Hangul Compatibility Jamo + ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ ㅤ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ +Kanbun + ㆐ ㆑ ㆒ ㆓ ㆔ ㆕ ㆖ ㆗ ㆘ ㆙ ㆚ ㆛ ㆜ ㆝ ㆞ ㆟ +Enclosed CJK Letters and Months + ㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛ ㈜ ㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㉿ ㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ... +CJK Compatibility + ㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿ ㎀ ㎁ ㎂ ㎃ ... +CJK Unified Ideographs + 一 丁 丂 七 丄 丅 丆 万 丈 三 上 下 丌 不 与 丏 丐 丑 丒 专 且 丕 世 丗 丘 丙 业 丛 东 丝 丞 丟 丠 両 丢 丣 两 严 並 丧 丨 丩 个 丫 丬 中 丮 丯 丰 丱 串 丳 临 丵 丶 丷 丸 丹 为 主 丼 丽 举 丿 乀 乁 乂 乃 乄 久 乆 乇 么 义 乊 之 乌 乍 乎 乏 乐 乑 乒 乓 乔 乕 乖 乗 乘 乙 乚 乛 乜 九 乞 也 习 乡 乢 乣 乤 乥 书 乧 乨 乩 乪 乫 乬 乭 乮 乯 买 乱 乲 乳 乴 乵 乶 乷 乸 乹 乺 乻 乼 乽 乾 乿 ... +Hangul Syllables + 가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 감 갑 값 갓 갔 강 갖 갗 갘 같 갚 갛 개 객 갞 갟 갠 갡 갢 갣 갤 갥 갦 갧 갨 갩 갪 갫 갬 갭 갮 갯 갰 갱 갲 갳 갴 갵 갶 갷 갸 갹 갺 갻 갼 갽 갾 갿 걀 걁 걂 걃 걄 걅 걆 걇 걈 걉 걊 걋 걌 걍 걎 걏 걐 걑 걒 걓 걔 걕 걖 걗 걘 걙 걚 걛 걜 걝 걞 걟 걠 걡 걢 걣 걤 걥 걦 걧 걨 걩 걪 걫 걬 걭 걮 걯 거 걱 걲 걳 건 걵 걶 걷 걸 걹 걺 걻 걼 걽 걾 걿 ... +Private Use +                                                                                                                                 ... +CJK Compatibility Ideographs + 豈 更 車 賈 滑 串 句 龜 龜 契 金 喇 奈 懶 癩 羅 蘿 螺 裸 邏 樂 洛 烙 珞 落 酪 駱 亂 卵 欄 爛 蘭 鸞 嵐 濫 藍 襤 拉 臘 蠟 廊 朗 浪 狼 郎 來 冷 勞 擄 櫓 爐 盧 老 蘆 虜 路 露 魯 鷺 碌 祿 綠 菉 錄 鹿 論 壟 弄 籠 聾 牢 磊 賂 雷 壘 屢 樓 淚 漏 累 縷 陋 勒 肋 凜 凌 稜 綾 菱 陵 讀 拏 樂 諾 丹 寧 怒 率 異 北 磻 便 復 不 泌 數 索 參 塞 省 葉 說 殺 辰 沈 拾 若 掠 略 亮 兩 凉 梁 糧 良 諒 量 勵 ... +Alphabetic Presentation Forms + ff fi fl ffi ffl ſt st ﬓ ﬔ ﬕ ﬖ ﬗ ﬞ ײַ ﬠ ﬡ ﬢ ﬣ ﬤ ﬥ ﬦ ﬧ ﬨ ﬩ שׁ שׂ שּׁ שּׂ אַ אָ אּ בּ גּ דּ הּ וּ זּ טּ יּ ךּ כּ לּ מּ נּ סּ ףּ פּ צּ קּ רּ שּ תּ וֹ בֿ כֿ פֿ ﭏ +Arabic Presentation Forms-A + ﭐ ﭑ ﭒ ﭓ ﭔ ﭕ ﭖ ﭗ ﭘ ﭙ ﭚ ﭛ ﭜ ﭝ ﭞ ﭟ ﭠ ﭡ ﭢ ﭣ ﭤ ﭥ ﭦ ﭧ ﭨ ﭩ ﭪ ﭫ ﭬ ﭭ ﭮ ﭯ ﭰ ﭱ ﭲ ﭳ ﭴ ﭵ ﭶ ﭷ ﭸ ﭹ ﭺ ﭻ ﭼ ﭽ ﭾ ﭿ ﮀ ﮁ ﮂ ﮃ ﮄ ﮅ ﮆ ﮇ ﮈ ﮉ ﮊ ﮋ ﮌ ﮍ ﮎ ﮏ ﮐ ﮑ ﮒ ﮓ ﮔ ﮕ ﮖ ﮗ ﮘ ﮙ ﮚ ﮛ ﮜ ﮝ ﮞ ﮟ ﮠ ﮡ ﮢ ﮣ ﮤ ﮥ ﮦ ﮧ ﮨ ﮩ ﮪ ﮫ ﮬ ﮭ ﮮ ﮯ ﮰ ﮱ ﯓ ﯔ ﯕ ﯖ ﯗ ﯘ ﯙ ﯚ ﯛ ﯜ ﯝ ﯞ ﯟ ﯠ ﯡ ﯢ ﯣ ﯤ ﯥ ﯦ ﯧ ﯨ ﯩ ﯪ ﯫ ﯬ ﯭ ﯮ ﯯ ﯰ ... +Combining Half Marks + ︠ ︡ ︢ ︣ +CJK Compatibility Forms + ︰ ︱ ︲ ︳ ︴ ︵ ︶ ︷ ︸ ︹ ︺ ︻ ︼ ︽ ︾ ︿ ﹀ ﹁ ﹂ ﹃ ﹄ ﹉ ﹊ ﹋ ﹌ ﹍ ﹎ ﹏ +Small Form Variants + ﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹘ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟ ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹨ ﹩ ﹪ ﹫ +Arabic Presentation Forms-B + ﹰ ﹱ ﹲ ﹴ ﹶ ﹷ ﹸ ﹹ ﹺ ﹻ ﹼ ﹽ ﹾ ﹿ ﺀ ﺁ ﺂ ﺃ ﺄ ﺅ ﺆ ﺇ ﺈ ﺉ ﺊ ﺋ ﺌ ﺍ ﺎ ﺏ ﺐ ﺑ ﺒ ﺓ ﺔ ﺕ ﺖ ﺗ ﺘ ﺙ ﺚ ﺛ ﺜ ﺝ ﺞ ﺟ ﺠ ﺡ ﺢ ﺣ ﺤ ﺥ ﺦ ﺧ ﺨ ﺩ ﺪ ﺫ ﺬ ﺭ ﺮ ﺯ ﺰ ﺱ ﺲ ﺳ ﺴ ﺵ ﺶ ﺷ ﺸ ﺹ ﺺ ﺻ ﺼ ﺽ ﺾ ﺿ ﻀ ﻁ ﻂ ﻃ ﻄ ﻅ ﻆ ﻇ ﻈ ﻉ ﻊ ﻋ ﻌ ﻍ ﻎ ﻏ ﻐ ﻑ ﻒ ﻓ ﻔ ﻕ ﻖ ﻗ ﻘ ﻙ ﻚ ﻛ ﻜ ﻝ ﻞ ﻟ ﻠ ﻡ ﻢ ﻣ ﻤ ﻥ ﻦ ﻧ ﻨ ﻩ ﻪ ﻫ ﻬ ﻭ ﻮ ﻯ ﻰ ﻱ ... +Halfwidth and Fullwidth Forms + ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ... +Specials +  +Specials + � + diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt new file mode 100644 index 000000000..b7bf2d8b4 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt @@ -0,0 +1,156 @@ +package com.kunzisoft.keepass.tests.stream + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.database.BinaryAttachment +import com.kunzisoft.keepass.stream.readAllBytes +import com.kunzisoft.keepass.utils.UriUtil +import junit.framework.TestCase.assertEquals +import org.junit.Test +import java.io.DataInputStream +import java.io.File +import java.io.InputStream +import java.lang.Exception +import java.security.MessageDigest + +class BinaryAttachmentTest { + + private val context: Context by lazy { + InstrumentationRegistry.getInstrumentation().context + } + + private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext) + private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A) + private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B) + private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C) + + private val loadedKey = Database.LoadedKey.generateNewCipherKey() + + private fun saveBinary(asset: String, binaryAttachment: BinaryAttachment) { + context.assets.open(asset).use { assetInputStream -> + binaryAttachment.getOutputDataStream(loadedKey).use { binaryOutputStream -> + assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer -> + binaryOutputStream.write(buffer) + } + } + } + } + + @Test + fun testSaveTextInCache() { + val binaryA = BinaryAttachment(fileA) + val binaryB = BinaryAttachment(fileB) + saveBinary(TEST_TEXT_ASSET, binaryA) + saveBinary(TEST_TEXT_ASSET, binaryB) + assertEquals("Save text binary length failed.", binaryA.length, binaryB.length) + assertEquals("Save text binary MD5 failed.", binaryA.md5(), binaryB.md5()) + } + + @Test + fun testSaveImageInCache() { + val binaryA = BinaryAttachment(fileA) + val binaryB = BinaryAttachment(fileB) + saveBinary(TEST_IMAGE_ASSET, binaryA) + saveBinary(TEST_IMAGE_ASSET, binaryB) + assertEquals("Save image binary length failed.", binaryA.length, binaryB.length) + assertEquals("Save image binary failed.", binaryA.md5(), binaryB.md5()) + } + + @Test + fun testCompressText() { + val binaryA = BinaryAttachment(fileA) + val binaryB = BinaryAttachment(fileB) + val binaryC = BinaryAttachment(fileC) + saveBinary(TEST_TEXT_ASSET, binaryA) + saveBinary(TEST_TEXT_ASSET, binaryB) + saveBinary(TEST_TEXT_ASSET, binaryC) + binaryA.compress(loadedKey) + binaryB.compress(loadedKey) + assertEquals("Compress text length failed.", binaryA.length, binaryB.length) + assertEquals("Compress text MD5 failed.", binaryA.md5(), binaryB.md5()) + binaryB.decompress(loadedKey) + assertEquals("Decompress text length failed.", binaryB.length, binaryC.length) + assertEquals("Decompress text MD5 failed.", binaryB.md5(), binaryC.md5()) + } + + @Test + fun testCompressImage() { + val binaryA = BinaryAttachment(fileA) + var binaryB = BinaryAttachment(fileB) + val binaryC = BinaryAttachment(fileC) + saveBinary(TEST_IMAGE_ASSET, binaryA) + saveBinary(TEST_IMAGE_ASSET, binaryB) + saveBinary(TEST_IMAGE_ASSET, binaryC) + binaryA.compress(loadedKey) + binaryB.compress(loadedKey) + assertEquals("Compress image length failed.", binaryA.length, binaryA.length) + assertEquals("Compress image failed.", binaryA.md5(), binaryA.md5()) + binaryB = BinaryAttachment(fileB, true) + binaryB.decompress(loadedKey) + assertEquals("Decompress image length failed.", binaryB.length, binaryC.length) + assertEquals("Decompress image failed.", binaryB.md5(), binaryC.md5()) + } + + @Test + fun testReadText() { + val binaryA = BinaryAttachment(fileA) + saveBinary(TEST_TEXT_ASSET, binaryA) + assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET), + binaryA.getInputDataStream(loadedKey))) + } + + @Test + fun testReadImage() { + val binaryA = BinaryAttachment(fileA) + saveBinary(TEST_IMAGE_ASSET, binaryA) + assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET), + binaryA.getInputDataStream(loadedKey))) + } + + private fun streamAreEquals(inputStreamA: InputStream, + inputStreamB: InputStream): Boolean { + val bufferA = ByteArray(DEFAULT_BUFFER_SIZE) + val bufferB = ByteArray(DEFAULT_BUFFER_SIZE) + val dataInputStreamB = DataInputStream(inputStreamB) + try { + var len: Int + while (inputStreamA.read(bufferA).also { len = it } > 0) { + dataInputStreamB.readFully(bufferB, 0, len) + for (i in 0 until len) { + if (bufferA[i] != bufferB[i]) + return false + } + } + return inputStreamB.read() < 0 // is the end of the second file also. + } catch (e: Exception) { + return false + } + finally { + inputStreamA.close() + inputStreamB.close() + } + } + + private fun BinaryAttachment.md5(): String { + val md = MessageDigest.getInstance("MD5") + return this.getInputDataStream(loadedKey).use { fis -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + generateSequence { + when (val bytesRead = fis.read(buffer)) { + -1 -> null + else -> bytesRead + } + }.forEach { bytesRead -> md.update(buffer, 0, bytesRead) } + md.digest().joinToString("") { "%02x".format(it) } + } + } + + companion object { + private const val TEST_FILE_CACHE_A = "testA" + private const val TEST_FILE_CACHE_B = "testB" + private const val TEST_FILE_CACHE_C = "testC" + private const val TEST_IMAGE_ASSET = "test_image.png" + private const val TEST_TEXT_ASSET = "test_text.txt" + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4cdd71c4b..7bcdcb9a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -129,9 +129,12 @@ + + android:windowSoftInputMode="adjustResize" /> @@ -213,7 +216,7 @@ diff --git a/app/src/main/java/com/igreenwood/loupe/Loupe.kt b/app/src/main/java/com/igreenwood/loupe/Loupe.kt new file mode 100644 index 000000000..e94a84411 --- /dev/null +++ b/app/src/main/java/com/igreenwood/loupe/Loupe.kt @@ -0,0 +1,1061 @@ +/* + * Loop 1.2.1 created by Issei Aoki modified by Jeremy JAMET + * https://github.com/igreenwood/loupe + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Issei Aoki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.igreenwood.loupe + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import android.util.TypedValue +import android.view.* +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import android.widget.ImageView +import android.widget.OverScroller +import androidx.core.view.ViewCompat +import java.lang.ref.WeakReference +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + + +class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener, + View.OnLayoutChangeListener { + + companion object { + const val DEFAULT_MAX_ZOOM = 5.0f + const val DEFAULT_ANIM_DURATION = 250L + const val DEFAULT_ANIM_DURATION_LONG = 375L + const val DEFAULT_VIEW_DRAG_FRICTION = 1f + const val DEFAULT_DRAG_DISMISS_DISTANCE_IN_VIEW_HEIGHT_RATIO = 0.5f + const val DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP = 96 + const val MAX_FLING_VELOCITY = 8000f + const val MIN_FLING_VELOCITY = 1500f + const val DEFAULT_DOUBLE_TAP_ZOOM_SCALE = 0.5f + val DEFAULT_INTERPOLATOR = DecelerateInterpolator() + + fun create( + imageView: ImageView, + container: ViewGroup, + config: Loupe.() -> Unit = { } + ): Loupe { + return Loupe(imageView, container).apply(config) + } + } + + interface OnViewTranslateListener { + fun onStart(view: ImageView) + fun onViewTranslate(view: ImageView, amount: Float) + fun onDismiss(view: ImageView) + fun onRestore(view: ImageView) + } + + interface OnScaleChangedListener { + fun onScaleChange(scaleFactor: Float, focusX: Float, focusY: Float) + } + + // max zoom(> 1f) + var maxZoom = DEFAULT_MAX_ZOOM + // use fling gesture for dismiss + var useFlingToDismissGesture = true + // flag to enable or disable drag to dismiss + var useDragToDismiss = true + // duration millis for dismiss animation + var dismissAnimationDuration = DEFAULT_ANIM_DURATION + // duration millis for restore animation + var restoreAnimationDuration = DEFAULT_ANIM_DURATION + // duration millis for image animation + var flingAnimationDuration = DEFAULT_ANIM_DURATION + // duration millis for double tap scale animation + var scaleAnimationDuration = DEFAULT_ANIM_DURATION_LONG + // duration millis for over scale animation + var overScaleAnimationDuration = DEFAULT_ANIM_DURATION_LONG + // duration millis for over scrolling animation + var overScrollAnimationDuration = DEFAULT_ANIM_DURATION + // view drag friction for swipe to dismiss(1f : drag distance == view move distance. Smaller value, view is moving more slower) + var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION + // drag distance threshold in dp for swipe to dismiss + var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP + // on view translate listener + var onViewTranslateListener: OnViewTranslateListener? = null + // on scale changed + var onScaleChangedListener: OnScaleChangedListener? = null + + var dismissAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR + + var restoreAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR + + var flingAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR + + var doubleTapScaleAnimationInterpolator: Interpolator = AccelerateDecelerateInterpolator() + + var overScaleAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR + + var overScrollAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR + + var doubleTapZoomScale: Float = DEFAULT_DOUBLE_TAP_ZOOM_SCALE // 0f~1f + + var minimumFlingVelocity: Float = MIN_FLING_VELOCITY + + private var flingAnimator: Animator = ValueAnimator() + + // bitmap matrix + private var transfrom = Matrix() + // bitmap scale + private var scale = 1f + // is ready for drawing bitmap + private var isReadyToDraw = false + // view rect - padding (recalculated on size changed) + private var canvasBounds = RectF() + // bitmap drawing rect (move on scroll, recalculated on scale changed) + private var bitmapBounds = RectF() + // displaying bitmap rect (does not move, recalculated on scale changed) + private var viewport = RectF() + // minimum scale of bitmap + private var minScale = 1f + // maximum scale of bitmap + private var maxScale = 1f + // bitmap (decoded) width + private var imageWidth = 0f + // bitmap (decoded) height + private var imageHeight = 0f + + private val scroller: OverScroller + private var originalViewBounds = Rect() + private var dragToDismissThreshold = 0f + private var dragDismissDistanceInPx = 0f + private var isViewTranslateAnimationRunning = false + private var isVerticalScrollEnabled = true + private var isHorizontalScrollEnabled = true + private var isBitmapTranslateAnimationRunning = false + private var isBitmapScaleAnimationRunninng = false + private var initialY = 0f + // scaling helper + private var scaleGestureDetector: ScaleGestureDetector? = null + // translating helper + private var gestureDetector: GestureDetector? = null + private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener = + object : ScaleGestureDetector.OnScaleGestureListener { + + override fun onScale(detector: ScaleGestureDetector?): Boolean { + if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) { + return false + } + + val scaleFactor = detector?.scaleFactor ?: 1.0f + val focalX = detector?.focusX ?: bitmapBounds.centerX() + val focalY = detector?.focusY ?: bitmapBounds.centerY() + + if (detector?.scaleFactor == 1.0f) { + // scale is not changing + return true + } + + zoomToTargetScale(calcNewScale(scaleFactor), focalX, focalY) + + return true + } + + override fun onScaleBegin(p0: ScaleGestureDetector?): Boolean = true + + override fun onScaleEnd(p0: ScaleGestureDetector?) {} + } + + private val onGestureListener: GestureDetector.OnGestureListener = + object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent?): Boolean = true + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent?, + distanceX: Float, + distanceY: Float + ): Boolean { + if (e2?.pointerCount != 1) { + return true + } + + if (scale > minScale) { + processScroll(distanceX, distanceY) + } else if (useDragToDismiss && scale == minScale) { + processDrag(distanceY) + } + return true + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + e1 ?: return true + + if (scale > minScale) { + processFlingBitmap(velocityX, velocityY) + } else { + processFlingToDismiss(velocityY) + } + return true + } + + override fun onDoubleTap(e: MotionEvent?): Boolean { + e ?: return false + + if (isBitmapScaleAnimationRunninng) { + return true + } + + if (scale > minScale) { + zoomOutToMinimumScale() + } else { + zoomInToTargetScale(e) + } + return true + } + } + + init { + container.apply { + background.alpha = 255 + setOnTouchListener(this@Loupe) + addOnLayoutChangeListener(this@Loupe) + scaleGestureDetector = ScaleGestureDetector(context, onScaleGestureListener) + gestureDetector = GestureDetector(context, onGestureListener) + scroller = OverScroller(context) + dragDismissDistanceInPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dragDismissDistanceInDp.toFloat(), + resources.displayMetrics + ) + } + imageView.apply { + imageMatrix = null + y = 0f + animate().cancel() + scaleType = ImageView.ScaleType.MATRIX + } + } + + private var imageViewRef: WeakReference = WeakReference(imageView) + private var containerRef: WeakReference = WeakReference(container) + + override fun onTouch(view: View?, event: MotionEvent?): Boolean { + event ?: return false + val imageView = imageViewRef.get() ?: return false + val container = containerRef.get() ?: return false + + container.parent.requestDisallowInterceptTouchEvent(scale != minScale) + + if (!imageView.isEnabled) { + return false + } + + if (isViewTranslateAnimationRunning) { + return false + } + + val scaleEvent = scaleGestureDetector?.onTouchEvent(event) + val isScaleAnimationIsRunning = scale < minScale + if (scaleEvent != scaleGestureDetector?.isInProgress && !isScaleAnimationIsRunning) { + // handle single touch gesture when scaling process is not running + gestureDetector?.onTouchEvent(event) + } + when (event.action) { + MotionEvent.ACTION_DOWN -> { + flingAnimator.cancel() + } + MotionEvent.ACTION_UP -> { + when { + scale == minScale -> { + if (!isViewTranslateAnimationRunning) { + dismissOrRestoreIfNeeded() + } + } + scale > minScale -> { + constrainBitmapBounds(true) + } + else -> { + zoomOutToMinimumScale(true) + } + } + } + } + + setTransform() + imageView.postInvalidate() + return true + } + + override fun onLayoutChange( + view: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + val imageView = imageViewRef.get() ?: return + val container = containerRef.get() ?: return + + imageView.run { + setupLayout(left, top, right, bottom) + initialY = y + if (useFlingToDismissGesture) { + setDragToDismissDistance(DEFAULT_DRAG_DISMISS_DISTANCE_IN_VIEW_HEIGHT_RATIO) + } else { + setDragToDismissDistance(DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP) + } + container.background.alpha = 255 + setTransform() + postInvalidate() + } + } + + private fun startVerticalTranslateAnimation(velY: Float) { + val imageView = imageViewRef.get() ?: return + + isViewTranslateAnimationRunning = true + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + imageView.run { + val translationY = if (velY > 0) { + originalViewBounds.top + height - top + } else { + originalViewBounds.top - height - top + } + animate() + .setDuration(dismissAnimationDuration) + .setInterpolator(dismissAnimationInterpolator) + .translationY(translationY.toFloat()) + .setUpdateListener { + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(imageView, amount) + } + .setListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + + } + + override fun onAnimationEnd(p0: Animator?) { + isViewTranslateAnimationRunning = false + onViewTranslateListener?.onDismiss(imageView) + cleanup() + } + + override fun onAnimationCancel(p0: Animator?) { + isViewTranslateAnimationRunning = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + } + } else { + ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) { + originalViewBounds.top + imageView.height - imageView.top + } else { + originalViewBounds.top - imageView.height - imageView.top + }.toFloat()).apply { + duration = dismissAnimationDuration + interpolator = dismissAnimationInterpolator + addUpdateListener { + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(imageView, amount) + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + // no op + } + + override fun onAnimationEnd(p0: Animator?) { + isViewTranslateAnimationRunning = false + onViewTranslateListener?.onDismiss(imageView) + cleanup() + } + + override fun onAnimationCancel(p0: Animator?) { + isViewTranslateAnimationRunning = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + start() + } + } + } + + private fun processFlingBitmap(velocityX: Float, velocityY: Float) { + val imageView = imageViewRef.get() ?: return + + var (velX, velY) = velocityX / scale to velocityY / scale + + if (velX == 0f && velY == 0f) { + return + } + + if (velX > MAX_FLING_VELOCITY) { + velX = MAX_FLING_VELOCITY + } + + if (velY > MAX_FLING_VELOCITY) { + velY = MAX_FLING_VELOCITY + } + + val (fromX, fromY) = bitmapBounds.left to bitmapBounds.top + + scroller.forceFinished(true) + scroller.fling( + fromX.roundToInt(), + fromY.roundToInt(), + velX.roundToInt(), + velY.roundToInt(), + (viewport.right - bitmapBounds.width()).roundToInt(), + viewport.left.roundToInt(), + (viewport.bottom - bitmapBounds.height()).roundToInt(), + viewport.top.roundToInt() + ) + + ViewCompat.postInvalidateOnAnimation(imageView) + + val toX = scroller.finalX.toFloat() + val toY = scroller.finalY.toFloat() + + flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = flingAnimationDuration + interpolator = flingAnimationInterpolator + addUpdateListener { + val amount = it.animatedValue as Float + val newLeft = lerp(amount, fromX, toX) + val newTop = lerp(amount, fromY, toY) + bitmapBounds.offsetTo(newLeft, newTop) + ViewCompat.postInvalidateOnAnimation(imageView) + setTransform() + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + isBitmapTranslateAnimationRunning = true + } + + override fun onAnimationEnd(p0: Animator?) { + isBitmapTranslateAnimationRunning = false + constrainBitmapBounds() + } + + override fun onAnimationCancel(p0: Animator?) { + isBitmapTranslateAnimationRunning = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + } + flingAnimator.start() + } + + private fun processScroll(distanceX: Float, distanceY: Float) { + val distX = if (isHorizontalScrollEnabled) { + -distanceX + } else { + 0f + } + val distY = if (isVerticalScrollEnabled) { + -distanceY + } else { + 0f + } + offsetBitmap(distX, distY) + setTransform() + } + + private fun zoomInToTargetScale(e: MotionEvent) { + val imageView = imageViewRef.get() ?: return + val startScale = scale + val endScale = minScale * maxZoom * doubleTapZoomScale + val focalX = e.x + val focalY = e.y + ValueAnimator.ofFloat(startScale, endScale).apply { + duration = scaleAnimationDuration + interpolator = doubleTapScaleAnimationInterpolator + addUpdateListener { + zoomToTargetScale(it.animatedValue as Float, focalX, focalY) + ViewCompat.postInvalidateOnAnimation(imageView) + setTransform() + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + isBitmapScaleAnimationRunninng = true + } + + override fun onAnimationEnd(p0: Animator?) { + isBitmapScaleAnimationRunninng = false + if (endScale == minScale) { + zoomToTargetScale(minScale, focalX, focalY) + imageView.postInvalidate() + } + } + + override fun onAnimationCancel(p0: Animator?) { + isBitmapScaleAnimationRunninng = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + }.start() + } + + private fun zoomOutToMinimumScale(isOverScaling: Boolean = false) { + val imageView = imageViewRef.get() ?: return + val startScale = scale + val endScale = minScale + val startLeft = bitmapBounds.left + val startTop = bitmapBounds.top + val endLeft = canvasBounds.centerX() - imageWidth * minScale * 0.5f + val endTop = canvasBounds.centerY() - imageHeight * minScale * 0.5f + ValueAnimator.ofFloat(0f, 1f).apply { + duration = if (isOverScaling) { + overScaleAnimationDuration + } else { + scaleAnimationDuration + } + interpolator = if (isOverScaling) { + overScaleAnimationInterpolator + } else { + doubleTapScaleAnimationInterpolator + } + addUpdateListener { + val value = it.animatedValue as Float + scale = lerp(value, startScale, endScale) + val newLeft = lerp(value, startLeft, endLeft) + val newTop = lerp(value, startTop, endTop) + calcBounds() + bitmapBounds.offsetTo(newLeft, newTop) + constrainBitmapBounds() + ViewCompat.postInvalidateOnAnimation(imageView) + setTransform() + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + isBitmapScaleAnimationRunninng = true + } + + override fun onAnimationEnd(p0: Animator?) { + isBitmapScaleAnimationRunninng = false + if (endScale == minScale) { + scale = minScale + calcBounds() + constrainBitmapBounds() + imageView.postInvalidate() + } + } + + override fun onAnimationCancel(p0: Animator?) { + isBitmapScaleAnimationRunninng = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + }.start() + } + + private var lastDistY = Float.NaN + + private fun processDrag(distanceY: Float) { + val imageView = imageViewRef.get() ?: return + + if (lastDistY.isNaN()) { + lastDistY = distanceY + return + } + + if (imageView.y == initialY) { + onViewTranslateListener?.onStart(imageView) + } + + imageView.y -= distanceY * viewDragFriction // if viewDragRatio is 1.0f, view translation speed is equal to user scrolling speed. + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(imageView, amount) + } + + private fun dismissOrRestoreIfNeeded() { + if (!isDragging() || isViewTranslateAnimationRunning) { + return + } + dismissOrRestore() + } + + private fun dismissOrRestore() { + val imageView = imageViewRef.get() ?: return + + if (shouldTriggerDragToDismissAnimation()) { + if (useFlingToDismissGesture) { + startDragToDismissAnimation() + } else { + onViewTranslateListener?.onDismiss(imageView) + cleanup() + } + } else { + restoreViewTransform() + } + } + + private fun shouldTriggerDragToDismissAnimation() = dragDistance() > dragToDismissThreshold + + private fun restoreViewTransform() { + val imageView = imageViewRef.get() ?: return + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + imageView.run { + animate() + .setDuration(restoreAnimationDuration) + .setInterpolator(restoreAnimationInterpolator) + .translationY((originalViewBounds.top - top).toFloat()) + .setUpdateListener { + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(this, amount) + } + .setListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + // no op + } + + override fun onAnimationEnd(p0: Animator?) { + onViewTranslateListener?.onRestore(imageView) + } + + override fun onAnimationCancel(p0: Animator?) { + // no op + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + } + } else { + ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply { + duration = restoreAnimationDuration + interpolator = restoreAnimationInterpolator + addUpdateListener { + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(imageView, amount) + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + // no op + } + + override fun onAnimationEnd(p0: Animator?) { + onViewTranslateListener?.onRestore(imageView) + } + + override fun onAnimationCancel(p0: Animator?) { + // no op + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + start() + } + } + } + + private fun startDragToDismissAnimation() { + val imageView = imageViewRef.get() ?: return + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + imageView.run { + val translationY = if (y - initialY > 0) { + originalViewBounds.top + height - top + } else { + originalViewBounds.top - height - top + } + animate() + .setDuration(dismissAnimationDuration) + .setInterpolator(AccelerateDecelerateInterpolator()) + .translationY(translationY.toFloat()) + .setUpdateListener { + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(this, amount) + } + .setListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + isViewTranslateAnimationRunning = true + } + + override fun onAnimationEnd(p0: Animator?) { + isViewTranslateAnimationRunning = false + onViewTranslateListener?.onDismiss(imageView) + cleanup() + } + + override fun onAnimationCancel(p0: Animator?) { + isViewTranslateAnimationRunning = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + } + } else { + ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply { + duration = dismissAnimationDuration + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener { + val amount = calcTranslationAmount() + changeBackgroundAlpha(amount) + onViewTranslateListener?.onViewTranslate(imageView, amount) + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) { + isViewTranslateAnimationRunning = true + } + + override fun onAnimationEnd(p0: Animator?) { + isViewTranslateAnimationRunning = false + onViewTranslateListener?.onDismiss(imageView) + cleanup() + } + + override fun onAnimationCancel(p0: Animator?) { + isViewTranslateAnimationRunning = false + } + + override fun onAnimationRepeat(p0: Animator?) { + // no op + } + }) + start() + } + } + } + + private fun processFlingToDismiss(velocityY: Float) { + if (useFlingToDismissGesture && !isViewTranslateAnimationRunning) { + if (abs(velocityY) < minimumFlingVelocity) { + return + } + startVerticalTranslateAnimation(velocityY) + } + } + + private fun calcTranslationAmount() = + constrain( + 0f, + norm(dragDistance(), 0f, originalViewBounds.height().toFloat()), + 1f + ) + + private fun dragDistance() = abs(viewOffsetY()) + + private fun isDragging() = dragDistance() > 0f + + private fun viewOffsetY() = imageViewRef.get()?.y ?: 0 - initialY + + /** + * targetScale: new scale + * focalX: focal x in current bitmapBounds + * focalY: focal y in current bitmapBounds + */ + private fun zoomToTargetScale(targetScale: Float, focalX: Float, focalY: Float) { + scale = targetScale + val lastBounds = RectF(bitmapBounds) + // scale has changed, recalculate bitmap bounds + calcBounds() + // offset to focalPoint + offsetToZoomFocalPoint(focalX, focalY, lastBounds, bitmapBounds) + onScaleChangedListener?.onScaleChange(targetScale, focalX, focalY) + } + + private fun setTransform() { + val imageView = imageViewRef.get() ?: return + transfrom.apply { + reset() + postTranslate(-imageWidth / 2, -imageHeight / 2) + postScale(scale, scale) + postTranslate(bitmapBounds.centerX(), bitmapBounds.centerY()) + } + imageView.imageMatrix = transfrom + } + + /** + * setup layout + */ + private fun setupLayout(left: Int, top: Int, right: Int, bottom: Int) { + val imageView = imageViewRef.get() ?: return + originalViewBounds.set(left, top, right, bottom) + imageView.run { + val drawable = imageViewRef.get()?.drawable + val bitmap = (drawable as? BitmapDrawable)?.bitmap + if (width == 0 || height == 0 || drawable == null) return + imageWidth = (bitmap?.width ?: drawable.intrinsicWidth).toFloat() + imageHeight = (bitmap?.height ?: drawable.intrinsicHeight).toFloat() + val canvasWidth = (width - paddingLeft - paddingRight).toFloat() + val canvasHeight = (height - paddingTop - paddingBottom).toFloat() + + calcScaleRange(canvasWidth, canvasHeight, imageWidth, imageHeight) + calcBounds() + constrainBitmapBounds() + isReadyToDraw = true + invalidate() + } + } + + private fun constrainBitmapBounds(animate: Boolean = false) { + val imageView = imageViewRef.get() ?: return + + if (isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) { + return + } + + val offset = PointF() + + // constrain viewport inside bitmap bounds + if (viewport.left < bitmapBounds.left) { + offset.x += viewport.left - bitmapBounds.left + } + + if (viewport.top < bitmapBounds.top) { + offset.y += viewport.top - bitmapBounds.top + } + + if (viewport.right > bitmapBounds.right) { + offset.x += viewport.right - bitmapBounds.right + } + + if (viewport.bottom > bitmapBounds.bottom) { + offset.y += viewport.bottom - bitmapBounds.bottom + } + + if (offset.equals(0f, 0f)) { + return + } + + if (animate) { + if (!isVerticalScrollEnabled) { + bitmapBounds.offset(0f, offset.y) + offset.y = 0f + } + + if (!isHorizontalScrollEnabled) { + bitmapBounds.offset(offset.x, 0f) + offset.x = 0f + } + + val start = RectF(bitmapBounds) + val end = RectF(bitmapBounds).apply { + offset(offset.x, offset.y) + } + ValueAnimator.ofFloat(0f, 1f).apply { + duration = overScrollAnimationDuration + interpolator = overScrollAnimationInterpolator + addUpdateListener { + val amount = it.animatedValue as Float + val newLeft = lerp(amount, start.left, end.left) + val newTop = lerp(amount, start.top, end.top) + bitmapBounds.offsetTo(newLeft, newTop) + ViewCompat.postInvalidateOnAnimation(imageView) + setTransform() + } + }.start() + } else { + bitmapBounds.offset(offset.x, offset.y) + } + } + + /** + * calc canvas/bitmap bounds + */ + private fun calcBounds() { + val imageView = imageViewRef.get() ?: return + + imageView.run { + // calc canvas bounds + canvasBounds = RectF( + paddingLeft.toFloat(), + paddingTop.toFloat(), + width - paddingRight.toFloat(), + height - paddingBottom.toFloat() + ) + } + // calc bitmap bounds + bitmapBounds = RectF( + canvasBounds.centerX() - imageWidth * scale * 0.5f, + canvasBounds.centerY() - imageHeight * scale * 0.5f, + canvasBounds.centerX() + imageWidth * scale * 0.5f, + canvasBounds.centerY() + imageHeight * scale * 0.5f + ) + // calc viewport + viewport = RectF( + max(canvasBounds.left, bitmapBounds.left), + max(canvasBounds.top, bitmapBounds.top), + min(canvasBounds.right, bitmapBounds.right), + min(canvasBounds.bottom, bitmapBounds.bottom) + ) + // check scroll availability + isHorizontalScrollEnabled = true + isVerticalScrollEnabled = true + + if (bitmapBounds.width() < canvasBounds.width()) { + isHorizontalScrollEnabled = false + } + + if (bitmapBounds.height() < canvasBounds.height()) { + isVerticalScrollEnabled = false + } + } + + private fun offsetBitmap(offsetX: Float, offsetY: Float) { + bitmapBounds.offset(offsetX, offsetY) + } + + /** + * calc min/max scale and set initial scale + */ + private fun calcScaleRange( + canvasWidth: Float, + canvasHeight: Float, + bitmapWidth: Float, + bitmapHeight: Float + ) { + val canvasRatio = canvasHeight / canvasWidth + val bitmapRatio = bitmapHeight / bitmapWidth + minScale = if (canvasRatio > bitmapRatio) { + canvasWidth / bitmapWidth + } else { + canvasHeight / bitmapHeight + } + scale = minScale + maxScale = minScale * maxZoom + } + + + private fun calcNewScale(newScale: Float): Float { + return min(maxScale, newScale * scale) + } + + private fun constrain(min: Float, value: Float, max: Float): Float { + return max(min(value, max), min) + } + + private fun offsetToZoomFocalPoint( + focalX: Float, + focalY: Float, + oldBounds: RectF, + newBounds: RectF + ) { + val oldX = constrain(viewport.left, focalX, viewport.right) + val oldY = constrain(viewport.top, focalY, viewport.bottom) + val newX = map(oldX, oldBounds.left, oldBounds.right, newBounds.left, newBounds.right) + val newY = map(oldY, oldBounds.top, oldBounds.bottom, newBounds.top, newBounds.bottom) + offsetBitmap(oldX - newX, oldY - newY) + } + + private fun map( + value: Float, + srcStart: Float, + srcStop: Float, + dstStart: Float, + dstStop: Float + ): Float { + if (srcStop - srcStart == 0f) { + return 0f + } + return ((value - srcStart) * (dstStop - dstStart) / (srcStop - srcStart)) + dstStart + } + + private fun lerp(amt: Float, start: Float, stop: Float): Float { + return start + (stop - start) * amt + } + + private fun norm(value: Float, start: Float, stop: Float): Float { + return value / (stop - start) + } + + private fun changeBackgroundAlpha(amount: Float) { + val container = containerRef.get() ?: return + val newAlpha = ((1.0f - amount) * 255).roundToInt() + container.background.mutate().alpha = newAlpha + } + + fun setDragToDismissDistance(distance: Int) { + val imageView = imageViewRef.get() ?: return + dragToDismissThreshold = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + distance.toFloat(), + imageView.context.resources.displayMetrics + ) + } + + fun setDragToDismissDistance(heightRatio: Float) { + val imageView = imageViewRef.get() ?: return + dragToDismissThreshold = imageView.height * heightRatio + } + + fun dismiss() { + // Animate down offscreen (the finish listener will call the cleanup method) + startVerticalTranslateAnimation(MIN_FLING_VELOCITY) + } + + fun cleanup() { + containerRef.get()?.apply { + setOnTouchListener(null) + removeOnLayoutChangeListener(null) + } + imageViewRef.clear() + containerRef.clear() + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 7da549d96..526736668 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -51,22 +51,19 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.StreamDirection -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService -import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.otp.OtpEntryFields +import com.kunzisoft.keepass.services.AttachmentFileNotificationService +import com.kunzisoft.keepass.services.ClipboardEntryNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.MenuUtil -import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.createDocument -import com.kunzisoft.keepass.utils.onCreateDocumentResult +import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.EntryContentsView -import com.kunzisoft.keepass.view.showActionError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded import java.util.* import kotlin.collections.HashMap @@ -129,6 +126,7 @@ class EntryActivity : LockingActivity() { historyView = findViewById(R.id.history_container) entryContentsView = findViewById(R.id.entry_contents) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) + entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey) entryProgress = findViewById(R.id.entry_progress) lockView = findViewById(R.id.lock_button) @@ -156,10 +154,11 @@ class EntryActivity : LockingActivity() { } ACTION_DATABASE_RELOAD_TASK -> { // Close the current activity + this.showActionErrorIfNeeded(result) finish() } } - coordinatorLayout?.showActionError(result) + coordinatorLayout?.showActionErrorIfNeeded(result) } } @@ -222,7 +221,9 @@ class EntryActivity : LockingActivity() { registerProgressTask() onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { - entryContentsView?.putAttachment(entryAttachmentState) + if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) { + entryContentsView?.putAttachment(entryAttachmentState) + } } } } @@ -349,7 +350,7 @@ class EntryActivity : LockingActivity() { // Assign dates entryContentsView?.assignCreationDate(entryInfo.creationTime) - entryContentsView?.assignModificationDate(entryInfo.modificationTime) + entryContentsView?.assignModificationDate(entryInfo.lastModificationTime) entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime) // Manage history diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 5e3172466..146dccd21 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -36,7 +36,6 @@ import android.widget.DatePicker import android.widget.TimePicker import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView @@ -57,22 +56,22 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.model.* -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService -import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpEntryFields +import com.kunzisoft.keepass.services.AttachmentFileNotificationService +import com.kunzisoft.keepass.services.ClipboardEntryNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.KeyboardEntryNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.view.ToolbarAction import com.kunzisoft.keepass.view.asError -import com.kunzisoft.keepass.view.showActionError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.updateLockPaddingLeft import org.joda.time.DateTime import java.util.* @@ -99,7 +98,7 @@ class EntryEditActivity : LockingActivity(), private var coordinatorLayout: CoordinatorLayout? = null private var scrollView: NestedScrollView? = null private var entryEditFragment: EntryEditFragment? = null - private var entryEditAddToolBar: Toolbar? = null + private var entryEditAddToolBar: ToolbarAction? = null private var validateButton: View? = null private var lockView: View? = null @@ -107,7 +106,7 @@ class EntryEditActivity : LockingActivity(), private var mSelectFileHelper: SelectFileHelper? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAllowMultipleAttachments: Boolean = false - private var mTempAttachments = ArrayList() + private var mTempAttachments = ArrayList() // Education private var entryEditActivityEducation: EntryEditActivityEducation? = null @@ -119,11 +118,12 @@ class EntryEditActivity : LockingActivity(), super.onCreate(savedInstanceState) setContentView(R.layout.activity_entry_edit) - val toolbar = findViewById(R.id.toolbar) - setSupportActionBar(toolbar) - supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp) + // Bottom Bar + entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar) + setSupportActionBar(entryEditAddToolBar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout) @@ -198,14 +198,14 @@ class EntryEditActivity : LockingActivity(), // Build fragment to manage entry modification entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment? if (entryEditFragment == null) { - entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo) + entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey) } supportFragmentManager.beginTransaction() .replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG) .commit() entryEditFragment?.apply { drawFactory = mDatabase?.drawFactory - setOnDateClickListener = View.OnClickListener { + setOnDateClickListener = { expiryTime.date.let { expiresDate -> val dateTime = DateTime(expiresDate) val defaultYear = dateTime.year @@ -236,51 +236,6 @@ class EntryEditActivity : LockingActivity(), mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments } - // Assign title - title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry) - - // Bottom Bar - entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar) - entryEditAddToolBar?.apply { - menuInflater.inflate(R.menu.entry_edit, menu) - - menu.findItem(R.id.menu_add_field).apply { - val allowCustomField = mDatabase?.allowEntryCustomFields() == true - isEnabled = allowCustomField - isVisible = allowCustomField - } - - // Attachment not compatible below KitKat - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - menu.findItem(R.id.menu_add_attachment).isVisible = false - } - - menu.findItem(R.id.menu_add_otp).apply { - val allowOTP = mDatabase?.allowOTP == true - isEnabled = allowOTP - // OTP not compatible below KitKat - isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT - } - - setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.menu_add_field -> { - addNewCustomField() - true - } - R.id.menu_add_attachment -> { - addNewAttachment(item) - true - } - R.id.menu_add_otp -> { - setupOTP() - true - } - else -> true - } - } - } - // To retrieve attachment mSelectFileHelper = SelectFileHelper(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this) @@ -338,10 +293,11 @@ class EntryEditActivity : LockingActivity(), } ACTION_DATABASE_RELOAD_TASK -> { // Close the current activity + this.showActionErrorIfNeeded(result) finish() } } - coordinatorLayout?.showActionError(result) + coordinatorLayout?.showActionErrorIfNeeded(result) } } @@ -398,29 +354,27 @@ class EntryEditActivity : LockingActivity(), when (entryAttachmentState.downloadState) { AttachmentState.START -> { entryEditFragment?.apply { - // When only one attachment is allowed - if (!mAllowMultipleAttachments) { - clearAttachments() - } putAttachment(entryAttachmentState) // Scroll to the attachment position getAttachmentViewPosition(entryAttachmentState) { scrollView?.smoothScrollTo(0, it.toInt()) } - } + } // Add in temp list + mTempAttachments.add(entryAttachmentState) } AttachmentState.IN_PROGRESS -> { entryEditFragment?.putAttachment(entryAttachmentState) } AttachmentState.COMPLETE -> { - entryEditFragment?.apply { - putAttachment(entryAttachmentState) - // Scroll to the attachment position - getAttachmentViewPosition(entryAttachmentState) { + entryEditFragment?.putAttachment(entryAttachmentState) { + entryEditFragment?.getAttachmentViewPosition(entryAttachmentState) { scrollView?.smoothScrollTo(0, it.toInt()) } } } + AttachmentState.CANCELED -> { + entryEditFragment?.removeAttachment(entryAttachmentState) + } AttachmentState.ERROR -> { entryEditFragment?.removeAttachment(entryAttachmentState) coordinatorLayout?.let { @@ -516,10 +470,12 @@ class EntryEditActivity : LockingActivity(), private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) { if (attachmentToUploadUri != null && attachment != null) { + // When only one attachment is allowed + if (!mAllowMultipleAttachments) { + entryEditFragment?.clearAttachments() + } // Start uploading in service mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment) - // Add in temp list - mTempAttachments.add(attachment) } } @@ -572,6 +528,7 @@ class EntryEditActivity : LockingActivity(), * Saves the new entry or update an existing entry in the database */ private fun saveEntry() { + mAttachmentFileBinderManager?.stopUploadAllAttachments() // Get the temp entry entryEditFragment?.getEntryInfo()?.let { newEntryInfo -> @@ -583,14 +540,34 @@ class EntryEditActivity : LockingActivity(), Entry(mEntry!!) }?.let { newEntry -> + // Do not save entry in upload progression + mTempAttachments.forEach { attachmentState -> + if (attachmentState.streamDirection == StreamDirection.UPLOAD) { + when (attachmentState.downloadState) { + AttachmentState.START, + AttachmentState.IN_PROGRESS, + AttachmentState.CANCELED, + AttachmentState.ERROR -> { + // Remove attachment not finished from info + newEntryInfo.attachments = newEntryInfo.attachments.toMutableList().apply { + remove(attachmentState.attachment) + } + } + else -> { + } + } + } + } + // Build info newEntry.setEntryInfo(mDatabase, newEntryInfo) // Delete temp attachment if not used - mTempAttachments.forEach { + mTempAttachments.forEach { tempAttachmentState -> + val tempAttachment = tempAttachmentState.attachment mDatabase?.binaryPool?.let { binaryPool -> - if (!newEntry.getAttachments(binaryPool).contains(it)) { - mDatabase?.removeAttachmentIfNotUsed(it) + if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { + mDatabase?.removeAttachmentIfNotUsed(tempAttachment) } } } @@ -619,12 +596,30 @@ class EntryEditActivity : LockingActivity(), override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) - MenuUtil.contributionMenuInflater(menuInflater, menu) + menuInflater.inflate(R.menu.entry_edit, menu) return true } - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + + menu?.findItem(R.id.menu_add_field)?.apply { + val allowCustomField = mDatabase?.allowEntryCustomFields() == true + isEnabled = allowCustomField + isVisible = allowCustomField + } + + // Attachment not compatible below KitKat + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + menu?.findItem(R.id.menu_add_attachment)?.isVisible = false + } + + menu?.findItem(R.id.menu_add_otp)?.apply { + val allowOTP = mDatabase?.allowOTP == true + isEnabled = allowOTP + // OTP not compatible below KitKat + isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + } + entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post { performedNextEducation(it) } } @@ -676,8 +671,16 @@ class EntryEditActivity : LockingActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_contribute -> { - MenuUtil.onContributionItemSelected(this) + R.id.menu_add_field -> { + addNewCustomField() + return true + } + R.id.menu_add_attachment -> { + addNewAttachment(item) + return true + } + R.id.menu_add_otp -> { + setupOTP() return true } android.R.id.home -> { @@ -787,6 +790,7 @@ class EntryEditActivity : LockingActivity(), .setMessage(R.string.discard_changes) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.discard) { _, _ -> + mAttachmentFileBinderManager?.stopUploadAllAttachments() backPressedAlreadyApproved = true approved.invoke() }.create().show() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt index acd3c5c37..98a975471 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt @@ -26,10 +26,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import android.widget.CompoundButton import android.widget.EditText import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator @@ -41,6 +39,7 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.education.EntryEditActivityEducation @@ -49,6 +48,7 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.view.ExpirationView import com.kunzisoft.keepass.view.applyFontVisibility import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.expand @@ -63,8 +63,7 @@ class EntryEditFragment: StylishFragment() { private lateinit var entryPasswordLayoutView: TextInputLayout private lateinit var entryPasswordView: EditText private lateinit var entryPasswordGeneratorView: View - private lateinit var entryExpiresCheckBox: CompoundButton - private lateinit var entryExpiresTextView: TextView + private lateinit var entryExpirationView: ExpirationView private lateinit var entryNotesView: EditText private lateinit var extraFieldsContainerView: View private lateinit var extraFieldsListView: ViewGroup @@ -75,10 +74,9 @@ class EntryEditFragment: StylishFragment() { private var fontInVisibility: Boolean = false private var iconColor: Int = 0 - private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH var drawFactory: IconDrawableFactory? = null - var setOnDateClickListener: View.OnClickListener? = null + var setOnDateClickListener: (() -> Unit)? = null var setOnPasswordGeneratorClickListener: View.OnClickListener? = null var setOnIconViewClickListener: View.OnClickListener? = null var setOnEditCustomField: ((Field) -> Unit)? = null @@ -86,6 +84,7 @@ class EntryEditFragment: StylishFragment() { // Elements to modify the current entry private var mEntryInfo = EntryInfo() + private var mBinaryCipherKey: Database.LoadedKey? = null private var mLastFocusedEditField: FocusedEditField? = null private var mExtraViewToRequestFocus: EditText? = null @@ -112,12 +111,8 @@ class EntryEditFragment: StylishFragment() { entryPasswordGeneratorView.setOnClickListener { setOnPasswordGeneratorClickListener?.onClick(it) } - entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox) - entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text) - entryExpiresTextView.setOnClickListener { - if (entryExpiresCheckBox.isChecked) - setOnDateClickListener?.onClick(it) - } + entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration) + entryExpirationView.setOnDateClickListener = setOnDateClickListener entryNotesView = rootView.findViewById(R.id.entry_edit_notes) @@ -127,6 +122,7 @@ class EntryEditFragment: StylishFragment() { attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container) attachmentsListView = rootView.findViewById(R.id.entry_attachments_list) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) + attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey? attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize -> if (previousSize > 0 && newSize == 0) { attachmentsContainerView.collapse(true) @@ -140,10 +136,6 @@ class EntryEditFragment: StylishFragment() { (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false } - entryExpiresCheckBox.setOnCheckedChangeListener { _, _ -> - assignExpiresDateText() - } - // Retrieve the textColor to tint the icon val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE @@ -178,7 +170,7 @@ class EntryEditFragment: StylishFragment() { setOnEditCustomField = null } - fun getEntryInfo(): EntryInfo? { + fun getEntryInfo(): EntryInfo { populateEntryWithViews() return mEntryInfo } @@ -283,41 +275,20 @@ class EntryEditFragment: StylishFragment() { } } - private fun assignExpiresDateText() { - entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) { - entryExpiresTextView.setOnClickListener(setOnDateClickListener) - expiresInstant.getDateTimeString(resources) - } else { - entryExpiresTextView.setOnClickListener(null) - resources.getString(R.string.never) - } - if (fontInVisibility) - entryExpiresTextView.applyFontVisibility() - } - var expires: Boolean get() { - return entryExpiresCheckBox.isChecked + return entryExpirationView.expires } set(value) { - if (!value) { - expiresInstant = DateInstant.IN_ONE_MONTH - } - entryExpiresCheckBox.isChecked = value - assignExpiresDateText() + entryExpirationView.expires = value } var expiryTime: DateInstant get() { - return if (expires) - expiresInstant - else - DateInstant.NEVER_EXPIRE + return entryExpirationView.expiryTime } set(value) { - if (expires) - expiresInstant = value - assignExpiresDateText() + entryExpirationView.expiryTime = value } var notes: String @@ -502,9 +473,13 @@ class EntryEditFragment: StylishFragment() { return attachmentsAdapter.contains(attachment) } - fun putAttachment(attachment: EntryAttachmentState) { + fun putAttachment(attachment: EntryAttachmentState, + onPreviewLoaded: (()-> Unit)? = null) { attachmentsContainerView.visibility = View.VISIBLE attachmentsAdapter.putItem(attachment) + attachmentsAdapter.onBinaryPreviewLoaded = { + onPreviewLoaded?.invoke() + } } fun removeAttachment(attachment: EntryAttachmentState) { @@ -528,6 +503,7 @@ class EntryEditFragment: StylishFragment() { override fun onSaveInstanceState(outState: Bundle) { populateEntryWithViews() outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo) + outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey) outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField) super.onSaveInstanceState(outState) @@ -535,12 +511,15 @@ class EntryEditFragment: StylishFragment() { companion object { const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO" + const val KEY_BINARY_CIPHER_KEY = "KEY_BINARY_CIPHER_KEY" const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD" - fun getInstance(entryInfo: EntryInfo?): EntryEditFragment { + fun getInstance(entryInfo: EntryInfo?, + loadedKey: Database.LoadedKey?): EntryEditFragment { return EntryEditFragment().apply { arguments = Bundle().apply { putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo) + putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index a4d313091..e2c4672c5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -52,12 +52,13 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.asError @@ -199,8 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), when (actionTask) { ACTION_DATABASE_CREATE_TASK -> { result.data?.getParcelable(DATABASE_URI_KEY)?.let { databaseUri -> - val keyFileUri = result.data?.getParcelable(KEY_FILE_URI_KEY) - databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri) + val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential() + databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri) } } ACTION_DATABASE_LOAD_TASK -> { @@ -330,9 +331,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri) } - override fun onAssignKeyDialogPositiveClick( - masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) { + override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { try { mDatabaseFileUri?.let { databaseUri -> @@ -340,10 +339,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), // Create the new database mProgressDatabaseTaskProvider?.startDatabaseCreate( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } } catch (e: Exception) { @@ -353,11 +349,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } } - override fun onAssignKeyDialogNegativeClick( - masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) { - - } + override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 2ea93ef09..4a59aa394 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -19,7 +19,9 @@ package com.kunzisoft.keepass.activities import android.app.Activity +import android.app.DatePickerDialog import android.app.SearchManager +import android.app.TimePickerDialog import android.content.ComponentName import android.content.Context import android.content.Intent @@ -33,9 +35,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import android.widget.Toast +import android.widget.* import androidx.annotation.RequiresApi import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView @@ -53,37 +53,37 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.Entry -import com.kunzisoft.keepass.database.element.Group -import com.kunzisoft.keepass.database.element.SortNodeEnum -import com.kunzisoft.keepass.database.element.icon.IconImage +import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.icons.assignDatabaseIcon +import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.* +import org.joda.time.DateTime class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListener, IconPickerDialogFragment.IconPickerListener, + DatePickerDialog.OnDateSetListener, + TimePickerDialog.OnTimeSetListener, ListNodesFragment.NodeClickListener, ListNodesFragment.NodesActionMenuListener, DeleteNodesDialogFragment.DeleteNodeListener, @@ -345,13 +345,18 @@ class GroupActivity : LockingActivity(), } ACTION_DATABASE_RELOAD_TASK -> { // Reload the current activity - startActivity(intent) - finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + if (result.isSuccess) { + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } else { + this.showActionErrorIfNeeded(result) + finish() + } } } - coordinatorLayout?.showActionError(result) + coordinatorLayout?.showActionErrorIfNeeded(result) finishNodeAction() @@ -700,6 +705,39 @@ class GroupActivity : LockingActivity(), ) } + override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) { + // To fix android 4.4 issue + // https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice + if (datePicker?.isShown == true) { + val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment + groupEditFragment?.getExpiryTime()?.date?.let { expiresDate -> + groupEditFragment.setExpiryTime(DateInstant(DateTime(expiresDate) + .withYear(year) + .withMonthOfYear(month + 1) + .withDayOfMonth(day) + .toDate())) + // Launch the time picker + val dateTime = DateTime(expiresDate) + val defaultHour = dateTime.hourOfDay + val defaultMinute = dateTime.minuteOfHour + TimePickerFragment.getInstance(defaultHour, defaultMinute) + .show(supportFragmentManager, "TimePickerFragment") + } + } + } + + override fun onTimeSet(view: TimePicker?, hours: Int, minutes: Int) { + val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment + groupEditFragment?.getExpiryTime()?.date?.let { expiresDate -> + // Save the date + groupEditFragment.setExpiryTime( + DateInstant(DateTime(expiresDate) + .withHourOfDay(hours) + .withMinuteOfHour(minutes) + .toDate())) + } + } + private fun finishNodeAction() { actionNodeMode?.finish() } @@ -745,7 +783,7 @@ class GroupActivity : LockingActivity(), when (node.type) { Type.GROUP -> { mOldGroupToUpdate = node as Group - GroupEditDialogFragment.build(mOldGroupToUpdate!!) + GroupEditDialogFragment.build(mOldGroupToUpdate!!.getGroupInfo()) .show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) } @@ -1031,19 +1069,17 @@ class GroupActivity : LockingActivity(), } } - override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, - name: String?, - icon: IconImage?) { + override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, + groupInfo: GroupInfo) { - if (name != null && name.isNotEmpty() && icon != null) { + if (groupInfo.title.isNotEmpty()) { when (action) { GroupEditDialogFragment.EditGroupDialogAction.CREATION -> { // If group creation mCurrentGroup?.let { currentGroup -> // Build the group mDatabase?.createGroup()?.let { newGroup -> - newGroup.title = name - newGroup.icon = icon + newGroup.setGroupInfo(groupInfo) // Not really needed here because added in runnable but safe newGroup.parent = currentGroup @@ -1063,9 +1099,7 @@ class GroupActivity : LockingActivity(), // WARNING remove parent and children to keep memory removeParent() removeChildren() - - title = name - this.icon = icon // TODO custom icon #96 + this.setGroupInfo(groupInfo) } } // If group updated save it in the database @@ -1081,9 +1115,8 @@ class GroupActivity : LockingActivity(), } } - override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, - name: String?, - icon: IconImage?) { + override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, + groupInfo: GroupInfo) { // Do nothing here } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt new file mode 100644 index 000000000..5d12da108 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2021 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.format.Formatter +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import androidx.appcompat.widget.Toolbar +import com.igreenwood.loupe.Loupe +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database +import kotlinx.android.synthetic.main.activity_image_viewer.* + +class ImageViewerActivity : LockingActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_image_viewer) + + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + + val imageView: ImageView = findViewById(R.id.image_viewer_image) + val progressView: View = findViewById(R.id.image_viewer_progress) + + try { + progressView.visibility = View.VISIBLE + intent.getParcelableExtra(IMAGE_ATTACHMENT_TAG)?.let { attachment -> + + supportActionBar?.title = attachment.name + supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryAttachment.length) + + Attachment.loadBitmap(attachment, Database.getInstance().loadedCipherKey) { bitmapLoaded -> + if (bitmapLoaded == null) { + finish() + } else { + progressView.visibility = View.GONE + imageView.setImageBitmap(bitmapLoaded) + } + } + } ?: finish() + } catch (e: Exception) { + Log.e(TAG, "Unable to view the binary", e) + finish() + } + + Loupe.create(imageView, image_viewer_container) { + onViewTranslateListener = object : Loupe.OnViewTranslateListener { + + override fun onStart(view: ImageView) { + // called when the view starts moving + } + + override fun onViewTranslate(view: ImageView, amount: Float) { + // called whenever the view position changed + } + + override fun onRestore(view: ImageView) { + // called when the view drag gesture ended + } + + override fun onDismiss(view: ImageView) { + // called when the view drag gesture ended + finish() + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> finish() + } + return super.onOptionsItemSelected(item) + } + + companion object { + + private val TAG = ImageViewerActivity::class.simpleName + + private const val IMAGE_ATTACHMENT_TAG = "IMAGE_ATTACHMENT_TAG" + + fun getInstance(context: Context, imageAttachment: Attachment) { + context.startActivity(Intent(context, ImageViewerActivity::class.java).apply { + putExtra(IMAGE_ATTACHMENT_TAG, imageAttachment) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index fd514a1a7..502a06581 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -56,14 +56,14 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.MenuUtil @@ -236,15 +236,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil showLoadDatabaseDuplicateUuidMessage { var databaseUri: Uri? = null - var masterPassword: String? = null - var keyFileUri: Uri? = null + var mainCredential: MainCredential = MainCredential() var readOnly = true var cipherEntity: CipherDatabaseEntity? = null result.data?.let { resultData -> databaseUri = resultData.getParcelable(DATABASE_URI_KEY) - masterPassword = resultData.getString(MASTER_PASSWORD_KEY) - keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY) + mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential readOnly = resultData.getBoolean(READ_ONLY_KEY) cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY) } @@ -252,8 +250,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil databaseUri?.let { databaseFileUri -> showProgressDialogAndLoadDatabase( databaseFileUri, - masterPassword, - keyFileUri, + mainCredential, readOnly, cipherEntity, true) @@ -534,8 +531,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil // Show the progress dialog and load the database showProgressDialogAndLoadDatabase( databaseUri, - password, - keyFileUri, + MainCredential(password, keyFileUri), readOnly, cipherDatabaseEntity, false) @@ -544,15 +540,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil } private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, - password: String?, - keyFile: Uri?, + mainCredential: MainCredential, readOnly: Boolean, cipherDatabaseEntity: CipherDatabaseEntity?, fixDuplicateUUID: Boolean) { mProgressDatabaseTaskProvider?.startDatabaseLoad( databaseUri, - password, - keyFile, + mainCredential, readOnly, cipherDatabaseEntity, fixDuplicateUUID diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index cabface80..857176e73 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -37,6 +37,7 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.SelectFileHelper +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView @@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } interface AssignPasswordDialogListener { - fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) - fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) + fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) + fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) } override fun onAttach(activity: Context) { @@ -161,17 +160,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } } if (!error) { - mListener?.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) dismiss() } } val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) negativeButton.setOnClickListener { - mListener?.onAssignKeyDialogNegativeClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential()) dismiss() } } @@ -183,6 +178,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() { return super.onCreateDialog(savedInstanceState) } + private fun retrieveMainCredential(): MainCredential { + val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null + val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null + return MainCredential(masterPassword, keyFile) + } + override fun onResume() { super.onResume() @@ -242,9 +243,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { builder.setMessage(R.string.warning_empty_password) .setPositiveButton(android.R.string.ok) { _, _ -> if (!verifyKeyFile()) { - mListener?.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) this@AssignMasterKeyDialogFragment.dismiss() } } @@ -259,9 +258,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(it) builder.setMessage(R.string.warning_no_encryption_key) .setPositiveButton(android.R.string.ok) { _, _ -> - mListener?.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) this@AssignMasterKeyDialogFragment.dismiss() } .setNegativeButton(android.R.string.cancel) { _, _ -> } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt index d2cf2a1a5..b278bf815 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt @@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.node.Node -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle open class DeleteNodesDialogFragment : DialogFragment() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt index bd5f945d3..542e8dcb9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.node.Node -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt index a6b7a95c9..64082dd75 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt @@ -23,34 +23,39 @@ import android.app.Dialog import android.content.Context import android.graphics.Color import android.os.Bundle -import com.google.android.material.textfield.TextInputLayout -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog +import android.view.View import android.widget.Button import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.Group -import com.kunzisoft.keepass.database.element.icon.IconImage +import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.icons.assignDatabaseIcon +import com.kunzisoft.keepass.model.GroupInfo +import com.kunzisoft.keepass.view.ExpirationView +import org.joda.time.DateTime class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener { private var mDatabase: Database? = null - private var editGroupListener: EditGroupListener? = null + private var mEditGroupListener: EditGroupListener? = null - private var editGroupDialogAction: EditGroupDialogAction? = null - private var nameGroup: String? = null - private var iconGroup: IconImage? = null + private var mEditGroupDialogAction = EditGroupDialogAction.NONE + private var mGroupInfo = GroupInfo() - private var nameTextLayoutView: TextInputLayout? = null - private var nameTextView: TextView? = null - private var iconButtonView: ImageView? = null + private lateinit var iconButtonView: ImageView private var iconColor: Int = 0 + private lateinit var nameTextLayoutView: TextInputLayout + private lateinit var nameTextView: TextView + private lateinit var notesTextLayoutView: TextInputLayout + private lateinit var notesTextView: TextView + private lateinit var expirationView: ExpirationView enum class EditGroupDialogAction { CREATION, UPDATE, NONE; @@ -67,7 +72,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP // Verify that the host activity implements the callback interface try { // Instantiate the NoticeDialogListener so we can send events to the host - editGroupListener = context as EditGroupListener + mEditGroupListener = context as EditGroupListener } catch (e: ClassCastException) { // The activity doesn't implement the interface, throw exception throw ClassCastException(context.toString() @@ -76,16 +81,19 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP } override fun onDetach() { - editGroupListener = null + mEditGroupListener = null super.onDetach() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { activity?.let { activity -> val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null) - nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container) - nameTextView = root?.findViewById(R.id.group_edit_name) - iconButtonView = root?.findViewById(R.id.group_edit_icon_button) + iconButtonView = root.findViewById(R.id.group_edit_icon_button) + nameTextLayoutView = root.findViewById(R.id.group_edit_name_container) + nameTextView = root.findViewById(R.id.group_edit_name) + notesTextLayoutView = root.findViewById(R.id.group_edit_note_container) + notesTextView = root.findViewById(R.id.group_edit_note) + expirationView = root.findViewById(R.id.group_edit_expiration) // Retrieve the textColor to tint the icon val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) @@ -94,46 +102,48 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP // Init elements mDatabase = Database.getInstance() - editGroupDialogAction = EditGroupDialogAction.NONE - nameGroup = "" - iconGroup = mDatabase?.iconFactory?.folderIcon if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ACTION_ID) - && savedInstanceState.containsKey(KEY_NAME) - && savedInstanceState.containsKey(KEY_ICON)) { - editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID)) - nameGroup = savedInstanceState.getString(KEY_NAME) - iconGroup = savedInstanceState.getParcelable(KEY_ICON) - + && savedInstanceState.containsKey(KEY_GROUP_INFO)) { + mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID)) + mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo } else { arguments?.apply { if (containsKey(KEY_ACTION_ID)) - editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID)) - - if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) { - nameGroup = getString(KEY_NAME) - iconGroup = getParcelable(KEY_ICON) + mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID)) + if (mEditGroupDialogAction === CREATION) + mGroupInfo.notes = "" + if (containsKey(KEY_GROUP_INFO)) { + mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo } } } - // populate the name - nameTextView?.text = nameGroup - // populate the icon - assignIconView() + // populate info in views + populateInfoToViews() + expirationView.setOnDateClickListener = { + expirationView.expiryTime.date.let { expiresDate -> + val dateTime = DateTime(expiresDate) + val defaultYear = dateTime.year + val defaultMonth = dateTime.monthOfYear-1 + val defaultDay = dateTime.dayOfMonth + DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay) + .show(parentFragmentManager, "DatePickerFragment") + } + } val builder = AlertDialog.Builder(activity) builder.setView(root) .setPositiveButton(android.R.string.ok, null) .setNegativeButton(android.R.string.cancel) { _, _ -> - editGroupListener?.cancelEditGroup( - editGroupDialogAction, - nameTextView?.text?.toString(), - iconGroup) + retrieveGroupInfoFromViews() + mEditGroupListener?.cancelEditGroup( + mEditGroupDialogAction, + mGroupInfo) } - iconButtonView?.setOnClickListener { _ -> + iconButtonView.setOnClickListener { _ -> IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment") } @@ -150,55 +160,87 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP if (d != null) { val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button positiveButton.setOnClickListener { + retrieveGroupInfoFromViews() if (isValid()) { - editGroupListener?.approveEditGroup( - editGroupDialogAction, - nameTextView?.text?.toString(), - iconGroup) + mEditGroupListener?.approveEditGroup( + mEditGroupDialogAction, + mGroupInfo) d.dismiss() } } } } + fun getExpiryTime(): DateInstant { + retrieveGroupInfoFromViews() + return mGroupInfo.expiryTime + } + + fun setExpiryTime(expiryTime: DateInstant) { + mGroupInfo.expiryTime = expiryTime + populateInfoToViews() + } + + private fun populateInfoToViews() { + assignIconView() + nameTextView.text = mGroupInfo.title + notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE + mGroupInfo.notes?.let { + notesTextView.text = it + } + expirationView.expires = mGroupInfo.expires + expirationView.expiryTime = mGroupInfo.expiryTime + } + + private fun retrieveGroupInfoFromViews() { + mGroupInfo.title = nameTextView.text.toString() + // Only if there + val newNotes = notesTextView.text.toString() + if (newNotes.isNotEmpty()) { + mGroupInfo.notes = newNotes + } + mGroupInfo.expires = expirationView.expires + mGroupInfo.expiryTime = expirationView.expiryTime + } + private fun assignIconView() { - if (mDatabase?.drawFactory != null && iconGroup != null) { - iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor) + if (mDatabase?.drawFactory != null) { + iconButtonView.assignDatabaseIcon(mDatabase?.drawFactory!!, mGroupInfo.icon, iconColor) } } override fun iconPicked(bundle: Bundle) { - iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle) + mGroupInfo.icon = IconPickerDialogFragment.getIconStandardFromBundle(bundle) ?: mGroupInfo.icon assignIconView() } override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal) - outState.putString(KEY_NAME, nameGroup) - outState.putParcelable(KEY_ICON, iconGroup) + retrieveGroupInfoFromViews() + outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal) + outState.putParcelable(KEY_GROUP_INFO, mGroupInfo) super.onSaveInstanceState(outState) } private fun isValid(): Boolean { - if (nameTextView?.text?.toString()?.isNotEmpty() != true) { - nameTextLayoutView?.error = getString(R.string.error_no_name) + if (nameTextView.text.toString().isEmpty()) { + nameTextLayoutView.error = getString(R.string.error_no_name) return false } return true } interface EditGroupListener { - fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?) - fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?) + fun approveEditGroup(action: EditGroupDialogAction, + groupInfo: GroupInfo) + fun cancelEditGroup(action: EditGroupDialogAction, + groupInfo: GroupInfo) } companion object { const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" - - const val KEY_NAME = "KEY_NAME" - const val KEY_ICON = "KEY_ICON" const val KEY_ACTION_ID = "KEY_ACTION_ID" + const val KEY_GROUP_INFO = "KEY_GROUP_INFO" fun build(): GroupEditDialogFragment { val bundle = Bundle() @@ -208,11 +250,10 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP return fragment } - fun build(group: Group): GroupEditDialogFragment { + fun build(groupInfo: GroupInfo): GroupEditDialogFragment { val bundle = Bundle() - bundle.putString(KEY_NAME, group.title) - bundle.putParcelable(KEY_ICON, group.icon) bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal) + bundle.putParcelable(KEY_GROUP_INFO, groupInfo) val fragment = GroupEditDialogFragment() fragment.arguments = bundle return fragment diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt index 60da08681..e9f033e2d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt @@ -26,6 +26,7 @@ import android.net.Uri import android.os.Bundle import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.model.MainCredential class PasswordEncodingDialogFragment : DialogFragment() { @@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY) - val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false - val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY) - val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false - val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY) + val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential() activity?.let { activity -> val builder = AlertDialog.Builder(activity) @@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() { builder.setPositiveButton(android.R.string.ok) { _, _ -> mListener?.onPasswordEncodingValidateListener( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } @@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() { interface Listener { fun onPasswordEncodingValidateListener(databaseUri: Uri?, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) + mainCredential: MainCredential) } companion object { private const val DATABASE_URI_KEY = "DATABASE_URI_KEY" - private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" - private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY" - private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY" - private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY" + private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL" fun getInstance(databaseUri: Uri, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?): SortDialogFragment { + mainCredential: MainCredential): SortDialogFragment { val fragment = SortDialogFragment() fragment.arguments = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) - putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) - putString(MASTER_PASSWORD_KEY, masterPassword) - putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked) - putParcelable(KEY_FILE_URI_KEY, keyFile) + putParcelable(MAIN_CREDENTIAL, mainCredential) } return fragment } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt index 2315f9dbd..174904509 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt @@ -66,6 +66,7 @@ object Stylish { context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple + context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark else -> R.style.KeepassDXStyle_Light } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt index 0e276240e..011d4219c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/AnimatedItemsAdapter.kt @@ -120,7 +120,9 @@ abstract class AnimatedItemsAdapter(val contex } fun clear() { - itemsList.clear() - notifyDataSetChanged() + if (itemsList.size > 0) { + itemsList.clear() + notifyDataSetChanged() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt index 5564af29b..904aac8b6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt @@ -31,16 +31,22 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.ImageViewerActivity +import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.StreamDirection +import com.kunzisoft.keepass.view.expand class EntryAttachmentsItemsAdapter(context: Context) : AnimatedItemsAdapter(context) { + var binaryCipherKey: Database.LoadedKey? = null var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null + var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null private var mTitleColor: Int @@ -62,6 +68,37 @@ class EntryAttachmentsItemsAdapter(context: Context) val entryAttachmentState = itemsList[position] holder.itemView.visibility = View.VISIBLE + holder.binaryFileThumbnail.apply { + // Perform image loading only if upload is finished + if (entryAttachmentState.downloadState != AttachmentState.START + && entryAttachmentState.downloadState != AttachmentState.IN_PROGRESS) { + // Show the bitmap image if loaded + if (entryAttachmentState.previewState == AttachmentState.NULL) { + entryAttachmentState.previewState = AttachmentState.IN_PROGRESS + // Load the bitmap image + Attachment.loadBitmap(entryAttachmentState.attachment, binaryCipherKey) { imageLoaded -> + if (imageLoaded == null) { + entryAttachmentState.previewState = AttachmentState.ERROR + visibility = View.GONE + onBinaryPreviewLoaded?.invoke(entryAttachmentState) + } else { + entryAttachmentState.previewState = AttachmentState.COMPLETE + setImageBitmap(imageLoaded) + if (visibility != View.VISIBLE) { + expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) { + onBinaryPreviewLoaded?.invoke(entryAttachmentState) + } + } + } + } + } + } else { + visibility = View.GONE + } + this.setOnClickListener { + ImageViewerActivity.getInstance(context, entryAttachmentState.attachment) + } + } holder.binaryFileBroken.apply { setColorFilter(Color.RED) visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { @@ -77,7 +114,7 @@ class EntryAttachmentsItemsAdapter(context: Context) holder.binaryFileTitle.setTextColor(mTitleColor) } holder.binaryFileSize.text = Formatter.formatFileSize(context, - entryAttachmentState.attachment.binaryAttachment.length()) + entryAttachmentState.attachment.binaryAttachment.length) holder.binaryFileCompression.apply { if (entryAttachmentState.attachment.binaryAttachment.isCompressed) { text = CompressionAlgorithm.GZip.getName(context.resources) @@ -105,6 +142,7 @@ class EntryAttachmentsItemsAdapter(context: Context) } AttachmentState.NULL, AttachmentState.ERROR, + AttachmentState.CANCELED, AttachmentState.COMPLETE -> { holder.binaryFileProgressContainer.visibility = View.GONE holder.binaryFileProgress.visibility = View.GONE @@ -114,7 +152,7 @@ class EntryAttachmentsItemsAdapter(context: Context) } } } - holder.itemView.setOnClickListener(null) + holder.binaryFileInfo.setOnClickListener(null) } StreamDirection.DOWNLOAD -> { holder.binaryFileProgressIcon.isActivated = false @@ -122,12 +160,17 @@ class EntryAttachmentsItemsAdapter(context: Context) holder.binaryFileDeleteButton.visibility = View.GONE holder.binaryFileProgress.apply { visibility = when (entryAttachmentState.downloadState) { - AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE - AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE + AttachmentState.NULL, + AttachmentState.COMPLETE, + AttachmentState.CANCELED, + AttachmentState.ERROR -> View.GONE + + AttachmentState.START, + AttachmentState.IN_PROGRESS -> View.VISIBLE } progress = entryAttachmentState.downloadProgression } - holder.itemView.setOnClickListener { + holder.binaryFileInfo.setOnClickListener { onItemClickListener?.invoke(entryAttachmentState) } } @@ -136,6 +179,8 @@ class EntryAttachmentsItemsAdapter(context: Context) class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var binaryFileThumbnail: ImageView = itemView.findViewById(R.id.item_attachment_thumbnail) + var binaryFileInfo: View = itemView.findViewById(R.id.item_attachment_info) var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken) var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title) var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size) diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index 44927d4e9..f68ba9ea7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -57,7 +57,7 @@ class NodeAdapter (private val context: Context) private val mNodeSortedList: SortedList private val mInflater: LayoutInflater = LayoutInflater.from(context) - private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type + private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mPrefSizeMultiplier: Float = 0F private var mSubtextDefaultDimension: Float = 0F diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt index df871ebd5..eec183cda 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt @@ -25,7 +25,7 @@ import android.content.Intent import android.content.ServiceConnection import android.net.Uri import android.os.IBinder -import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService +import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.SingletonHolderParameter import java.util.* diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt index 7c001c6a4..83b77a33b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt @@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation -import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService +import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.AdvancedUnlockInfoView diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index c21307375..3bb1ed0f7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -24,39 +24,26 @@ import android.net.Uri import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil open class AssignPasswordInDatabaseRunnable ( context: Context, database: Database, protected val mDatabaseUri: Uri, - withMasterPassword: Boolean, - masterPassword: String?, - withKeyFile: Boolean, - keyFile: Uri?) + protected val mMainCredential: MainCredential) : SaveDatabaseRunnable(context, database, true) { - private var mMasterPassword: String? = null - protected var mKeyFileUri: Uri? = null - private var mBackupKey: ByteArray? = null - init { - if (withMasterPassword) - this.mMasterPassword = masterPassword - if (withKeyFile) - this.mKeyFileUri = keyFile - } - override fun onStartRun() { // Set key try { - // TODO move master key methods mBackupKey = ByteArray(database.masterKey.size) System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) - val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri) - database.retrieveMasterKey(mMasterPassword, uriInputStream) + val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri) + database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream) } catch (e: Exception) { erase(mBackupKey) setError(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index f2763f30a..0b55a1bb7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -24,6 +24,7 @@ import android.net.Uri import android.util.Log import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.UriUtil @@ -32,12 +33,9 @@ class CreateDatabaseRunnable(context: Context, databaseUri: Uri, private val databaseName: String, private val rootName: String, - withMasterPassword: Boolean, - masterPassword: String?, - withKeyFile: Boolean, - keyFile: Uri?, + mainCredential: MainCredential, private val createDatabaseResult: ((Result) -> Unit)?) - : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) { + : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { override fun onStartRun() { try { @@ -61,7 +59,7 @@ class CreateDatabaseRunnable(context: Context, if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context.applicationContext) .addOrUpdateDatabaseUri(mDatabaseUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null) + if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) } // Register the current time to init the lock timer diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index f732f3779..a5c10b9bf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater @@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.UriUtil class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, private val mUri: Uri, - private val mPass: String?, - private val mKey: Uri?, + private val mMainCredential: MainCredential, private val mReadonly: Boolean, private val mCipherEntity: CipherDatabaseEntity?, private val mFixDuplicateUUID: Boolean, @@ -51,10 +50,12 @@ class LoadDatabaseRunnable(private val context: Context, override fun onActionRun() { try { - mDatabase.loadData(mUri, mPass, mKey, + mDatabase.loadData(mUri, + mMainCredential, mReadonly, context.contentResolver, UriUtil.getBinaryDir(context), + Database.LoadedKey.generateNewCipherKey(), mFixDuplicateUUID, progressTaskUpdater) } @@ -67,7 +68,7 @@ class LoadDatabaseRunnable(private val context: Context, if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context) .addOrUpdateDatabaseUri(mUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null) + if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) } // Register the biometric diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt index 534067403..2c68513f9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt @@ -37,36 +37,37 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG @@ -264,30 +265,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { */ fun startDatabaseCreate(databaseUri: Uri, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + mainCredential: MainCredential) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) - putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) - putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) - putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked) - putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) } , ACTION_DATABASE_CREATE_TASK) } fun startDatabaseLoad(databaseUri: Uri, - masterPassword: String?, - keyFile: Uri?, + mainCredential: MainCredential, readOnly: Boolean, cipherEntity: CipherDatabaseEntity?, fixDuplicateUuid: Boolean) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) - putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) - putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) @@ -303,17 +296,11 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { } fun startDatabaseAssignPassword(databaseUri: Uri, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + mainCredential: MainCredential) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) - putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) - putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) - putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked) - putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) } , ACTION_DATABASE_ASSIGN_PASSWORD_TASK) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt index 8951d3615..33e4c4def 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable @@ -34,7 +33,10 @@ class ReloadDatabaseRunnable(private val context: Context, private val mLoadDatabaseResult: ((Result) -> Unit)?) : ActionRunnable() { + private var tempCipherKey: Database.LoadedKey? = null + override fun onStartRun() { + tempCipherKey = mDatabase.loadedCipherKey // Clear before we load mDatabase.clear(UriUtil.getBinaryDir(context)) } @@ -43,9 +45,9 @@ class ReloadDatabaseRunnable(private val context: Context, try { mDatabase.reloadData(context.contentResolver, UriUtil.getBinaryDir(context), + tempCipherKey ?: Database.LoadedKey.generateNewCipherKey(), progressTaskUpdater) - } - catch (e: LoadDatabaseException) { + } catch (e: LoadDatabaseException) { setError(e) } @@ -53,6 +55,7 @@ class ReloadDatabaseRunnable(private val context: Context, // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(context) } else { + tempCipherKey = null mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt index 8b5ed3e7a..9c200d372 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Attachment.kt @@ -19,9 +19,12 @@ */ package com.kunzisoft.keepass.database.element +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.Parcel import android.os.Parcelable import com.kunzisoft.keepass.database.element.database.BinaryAttachment +import kotlinx.coroutines.* data class Attachment(var name: String, var binaryAttachment: BinaryAttachment) : Parcelable { @@ -65,5 +68,28 @@ data class Attachment(var name: String, override fun newArray(size: Int): Array { return arrayOfNulls(size) } + + fun loadBitmap(attachment: Attachment, + binaryCipherKey: Database.LoadedKey?, + actionOnFinish: (Bitmap?) -> Unit) { + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + val asyncResult: Deferred = async { + runCatching { + binaryCipherKey?.let { binaryKey -> + var bitmap: Bitmap? + attachment.binaryAttachment.getUnGzipInputDataStream(binaryKey).use { bitmapInputStream -> + bitmap = BitmapFactory.decodeStream(bitmapInputStream) + } + bitmap + } + }.getOrNull() + } + withContext(Dispatchers.Main) { + actionOnFinish(asyncResult.await()) + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 371358464..660ae0d16 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -41,12 +41,17 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.icons.IconDrawableFactory +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.stream.readBytes4ToUInt import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.UriUtil import java.io.* +import java.security.Key +import java.security.SecureRandom import java.util.* +import javax.crypto.KeyGenerator +import javax.crypto.spec.IvParameterSpec import kotlin.collections.ArrayList @@ -74,6 +79,19 @@ class Database { var loadTimestamp: Long? = null private set + /** + * Cipher key regenerated when the database is loaded and closed + * Can be used to temporarily store database elements + */ + var loadedCipherKey: LoadedKey? + private set(value) { + mDatabaseKDB?.loadedCipherKey = value + mDatabaseKDBX?.loadedCipherKey = value + } + get() { + return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey + } + val iconFactory: IconImageFactory get() { return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory() @@ -320,12 +338,26 @@ class Database { } fun createData(databaseUri: Uri, databaseName: String, rootName: String) { - setDatabaseKDBX(DatabaseKDBX(databaseName, rootName)) + val newDatabase = DatabaseKDBX(databaseName, rootName) + newDatabase.loadedCipherKey = LoadedKey.generateNewCipherKey() + setDatabaseKDBX(newDatabase) this.fileUri = databaseUri // Set Database state this.loaded = true } + class LoadedKey(val key: Key, val iv: ByteArray): Serializable { + companion object { + const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding" + + fun generateNewCipherKey(): LoadedKey { + val iv = ByteArray(8) + SecureRandom().nextBytes(iv) + return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv) + } + } + } + @Throws(LoadDatabaseException::class) private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, openDatabaseKDB: (InputStream) -> DatabaseKDB, @@ -366,16 +398,20 @@ class Database { loaded = true } catch (e: LoadDatabaseException) { throw e + } catch (e: Exception) { + throw LoadDatabaseException(e) } finally { databaseInputStream?.close() } } @Throws(LoadDatabaseException::class) - fun loadData(uri: Uri, password: String?, keyfile: Uri?, + fun loadData(uri: Uri, + mainCredential: MainCredential, readOnly: Boolean, contentResolver: ContentResolver, cacheDirectory: File, + tempCipherKey: LoadedKey, fixDuplicateUUID: Boolean, progressTaskUpdater: ProgressTaskUpdater?) { @@ -389,8 +425,8 @@ class Database { var keyFileInputStream: InputStream? = null try { // Get keyFile inputStream - keyfile?.let { - keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) + mainCredential.keyFileUri?.let { keyFile -> + keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile) } // Read database stream for the first time @@ -398,16 +434,18 @@ class Database { { databaseInputStream -> DatabaseInputKDB(cacheDirectory) .openDatabase(databaseInputStream, - password, + mainCredential.masterPassword, keyFileInputStream, + tempCipherKey, progressTaskUpdater, fixDuplicateUUID) }, { databaseInputStream -> DatabaseInputKDBX(cacheDirectory) .openDatabase(databaseInputStream, - password, + mainCredential.masterPassword, keyFileInputStream, + tempCipherKey, progressTaskUpdater, fixDuplicateUUID) } @@ -427,27 +465,39 @@ class Database { @Throws(LoadDatabaseException::class) fun reloadData(contentResolver: ContentResolver, cacheDirectory: File, + tempCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?) { // Retrieve the stream from the old database URI - fileUri?.let { oldDatabaseUri -> - readDatabaseStream(contentResolver, oldDatabaseUri, - { databaseInputStream -> - DatabaseInputKDB(cacheDirectory) - .openDatabase(databaseInputStream, - masterKey, - progressTaskUpdater) - }, - { databaseInputStream -> - DatabaseInputKDBX(cacheDirectory) - .openDatabase(databaseInputStream, - masterKey, - progressTaskUpdater) - } - ) - } ?: run { - Log.e(TAG, "Database URI is null, database cannot be reloaded") - throw IODatabaseException() + try { + fileUri?.let { oldDatabaseUri -> + readDatabaseStream(contentResolver, oldDatabaseUri, + { databaseInputStream -> + DatabaseInputKDB(cacheDirectory) + .openDatabase(databaseInputStream, + masterKey, + tempCipherKey, + progressTaskUpdater) + }, + { databaseInputStream -> + DatabaseInputKDBX(cacheDirectory) + .openDatabase(databaseInputStream, + masterKey, + tempCipherKey, + progressTaskUpdater) + } + ) + } ?: run { + Log.e(TAG, "Database URI is null, database cannot be reloaded") + throw IODatabaseException() + } + } catch (e: FileNotFoundException) { + Log.e(TAG, "Unable to load keyfile", e) + throw FileNotFoundDatabaseException() + } catch (e: LoadDatabaseException) { + throw e + } catch (e: Exception) { + throw LoadDatabaseException(e) } } @@ -608,7 +658,9 @@ class Database { } } - fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { + fun validatePasswordEncoding(mainCredential: MainCredential): Boolean { + val password = mainCredential.masterPassword + val containsKeyFile = mainCredential.keyFileUri != null return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile) ?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile) ?: false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt index b985fbe38..56fcf7b17 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt @@ -346,12 +346,6 @@ class Entry : Node, EntryVersionedInterface { || entryKDBX?.containsAttachment() == true } - private fun addAttachments(binaryPool: BinaryPool, attachments: List) { - attachments.forEach { - putAttachment(it, binaryPool) - } - } - private fun removeAttachment(attachment: Attachment) { entryKDB?.removeAttachment(attachment) entryKDBX?.removeAttachment(attachment) @@ -427,7 +421,7 @@ class Entry : Node, EntryVersionedInterface { entryInfo.username = username entryInfo.password = password entryInfo.creationTime = creationTime - entryInfo.modificationTime = lastModificationTime + entryInfo.lastModificationTime = lastModificationTime entryInfo.expires = expires entryInfo.expiryTime = expiryTime entryInfo.url = url @@ -467,7 +461,9 @@ class Entry : Node, EntryVersionedInterface { notes = newEntryInfo.notes addExtraFields(newEntryInfo.customFields) database?.binaryPool?.let { binaryPool -> - addAttachments(binaryPool, newEntryInfo.attachments) + newEntryInfo.attachments.forEach { attachment -> + putAttachment(attachment, binaryPool) + } } database?.stopManageEntry(this) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt index 73ec4a35b..3478376db 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt @@ -29,6 +29,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.* import com.kunzisoft.keepass.model.EntryInfo +import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.settings.PreferencesUtil import java.util.* import kotlin.collections.ArrayList @@ -232,6 +233,14 @@ class Group : Node, GroupVersionedInterface { override val isCurrentlyExpires: Boolean get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false + var notes: String? + get() = groupKDBX?.notes + set(value) { + value?.let { + groupKDBX?.notes = it + } + } + override fun getChildGroups(): List { return groupKDB?.getChildGroups()?.map { Group(it) @@ -391,6 +400,35 @@ class Group : Node, GroupVersionedInterface { return groupKDBX?.containsCustomData() ?: false } + /* + ------------ + Converter + ------------ + */ + + fun getGroupInfo(): GroupInfo { + val groupInfo = GroupInfo() + groupInfo.title = title + groupInfo.icon = icon + groupInfo.creationTime = creationTime + groupInfo.lastModificationTime = lastModificationTime + groupInfo.expires = expires + groupInfo.expiryTime = expiryTime + groupInfo.notes = notes + return groupInfo + } + + fun setGroupInfo(groupInfo: GroupInfo) { + title = groupInfo.title + icon = groupInfo.icon + // Update date time, creation time stay as is + lastModificationTime = DateInstant() + lastAccessTime = DateInstant() + expires = groupInfo.expires + expiryTime = groupInfo.expiryTime + notes = groupInfo.notes + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt index 623d64412..9d3d5fc45 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt @@ -21,23 +21,33 @@ package com.kunzisoft.keepass.database.element.database import android.os.Parcel import android.os.Parcelable -import com.kunzisoft.keepass.stream.readBytes +import android.util.Base64 +import android.util.Base64InputStream +import android.util.Base64OutputStream +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.stream.readAllBytes +import org.apache.commons.io.output.CountingOutputStream import java.io.* import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.spec.IvParameterSpec class BinaryAttachment : Parcelable { private var dataFile: File? = null + var length: Long = 0 + private set var isCompressed: Boolean = false private set var isProtected: Boolean = false private set var isCorrupted: Boolean = false - - fun length(): Long { - return dataFile?.length() ?: 0 - } + // Cipher to encrypt temp file + private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER) + private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER) /** * Empty protected binary @@ -46,6 +56,7 @@ class BinaryAttachment : Parcelable { constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) { this.dataFile = dataFile + this.length = 0 this.isCompressed = compressed this.isProtected = protected } @@ -54,58 +65,77 @@ class BinaryAttachment : Parcelable { parcel.readString()?.let { dataFile = File(it) } + length = parcel.readLong() isCompressed = parcel.readByte().toInt() != 0 isProtected = parcel.readByte().toInt() != 0 isCorrupted = parcel.readByte().toInt() != 0 } @Throws(IOException::class) - fun getInputDataStream(): InputStream { + fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream { + return buildInputStream(dataFile!!, cipherKey) + } + + @Throws(IOException::class) + fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream { + return buildOutputStream(dataFile!!, cipherKey) + } + + @Throws(IOException::class) + fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream { + return if (isCompressed) { + GZIPInputStream(getInputDataStream(cipherKey)) + } else { + getInputDataStream(cipherKey) + } + } + + @Throws(IOException::class) + fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream { + return if (isCompressed) { + GZIPOutputStream(getOutputDataStream(cipherKey)) + } else { + getOutputDataStream(cipherKey) + } + } + + @Throws(IOException::class) + private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream { return when { - length() > 0 -> FileInputStream(dataFile!!) + file != null && file.length() > 0 -> { + cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv)) + Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP) + } else -> ByteArrayInputStream(ByteArray(0)) } } @Throws(IOException::class) - fun getUnGzipInputDataStream(): InputStream { - return if (isCompressed) - GZIPInputStream(getInputDataStream()) - else - getInputDataStream() - } - - @Throws(IOException::class) - fun getOutputDataStream(): OutputStream { + private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream { return when { - dataFile != null -> FileOutputStream(dataFile!!) + file != null -> { + cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv)) + BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP)) + } else -> throw IOException("Unable to write in an unknown file") } } @Throws(IOException::class) - fun getGzipOutputDataStream(): OutputStream { - return if (isCompressed) { - GZIPOutputStream(getOutputDataStream()) - } else { - getOutputDataStream() - } - } - - @Throws(IOException::class) - fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { + fun compress(cipherKey: Database.LoadedKey) { dataFile?.let { concreteDataFile -> // To compress, create a new binary with file if (!isCompressed) { + // Encrypt the new gzipped temp file val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") - GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream -> - getInputDataStream().use { inputStream -> - inputStream.readBytes(bufferSize) { buffer -> + getInputDataStream(cipherKey).use { inputStream -> + GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream -> + inputStream.readAllBytes { buffer -> outputStream.write(buffer) } } } - // Remove unGzip file + // Remove ungzip file if (concreteDataFile.delete()) { if (fileBinaryCompress.renameTo(concreteDataFile)) { // Harmonize with database compression @@ -117,13 +147,14 @@ class BinaryAttachment : Parcelable { } @Throws(IOException::class) - fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { + fun decompress(cipherKey: Database.LoadedKey) { dataFile?.let { concreteDataFile -> if (isCompressed) { + // Encrypt the new ungzipped temp file val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") - FileOutputStream(fileBinaryDecompress).use { outputStream -> - getUnGzipInputDataStream().use { inputStream -> - inputStream.readBytes(bufferSize) { buffer -> + getUnGzipInputDataStream(cipherKey).use { inputStream -> + buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream -> + inputStream.readAllBytes { buffer -> outputStream.write(buffer) } } @@ -170,6 +201,7 @@ class BinaryAttachment : Parcelable { result = 31 * result + if (isProtected) 1 else 0 result = 31 * result + if (isCorrupted) 1 else 0 result = 31 * result + dataFile!!.hashCode() + result = 31 * result + length.hashCode() return result } @@ -183,11 +215,31 @@ class BinaryAttachment : Parcelable { override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(dataFile?.absolutePath) + dest.writeLong(length) dest.writeByte((if (isCompressed) 1 else 0).toByte()) dest.writeByte((if (isProtected) 1 else 0).toByte()) dest.writeByte((if (isCorrupted) 1 else 0).toByte()) } + /** + * Custom OutputStream to calculate the size of binary file + */ + private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) { + init { + length = 0 + } + + override fun beforeWrite(n: Int) { + super.beforeWrite(n) + length = byteCount + } + + override fun close() { + super.close() + length = byteCount + } + } + companion object { private val TAG = BinaryAttachment::class.java.name diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt index 8678e84ce..a72875753 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt @@ -281,7 +281,5 @@ class DatabaseKDB : DatabaseVersioned() { const val BACKUP_FOLDER_TITLE = "Backup" private const val BACKUP_FOLDER_UNDEFINED_ID = -1 - - const val BUFFER_SIZE_BYTES = 3 * 128 } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index ded08f2d4..d2ebabd20 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -126,6 +126,7 @@ class DatabaseKDBX : DatabaseVersioned { */ constructor(databaseName: String, rootName: String) { name = databaseName + kdbxVersion = FILE_VERSION_32_3 val group = createGroup().apply { title = rootName icon = iconFactory.folderIcon @@ -212,8 +213,10 @@ class DatabaseKDBX : DatabaseVersioned { private fun compressAllBinaries() { binaryPool.doForEachBinary { binary -> try { + val cipherKey = loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to compress binaries") // To compress, create a new binary with file - binary.compress(BUFFER_SIZE_BYTES) + binary.compress(cipherKey) } catch (e: Exception) { Log.e(TAG, "Unable to compress $binary", e) } @@ -223,7 +226,9 @@ class DatabaseKDBX : DatabaseVersioned { private fun decompressAllBinaries() { binaryPool.doForEachBinary { binary -> try { - binary.decompress(BUFFER_SIZE_BYTES) + val cipherKey = loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to decompress binaries") + binary.decompress(cipherKey) } catch (e: Exception) { Log.e(TAG, "Unable to decompress $binary", e) } @@ -708,7 +713,5 @@ class DatabaseKDBX : DatabaseVersioned { private const val XML_ATTRIBUTE_DATA_HASH = "Hash" const val BASE_64_FLAG = Base64.NO_WRAP - - const val BUFFER_SIZE_BYTES = 3 * 128 } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt index 660434e5f..560d8968e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.database.element.database import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.group.GroupVersioned import com.kunzisoft.keepass.database.element.icon.IconImageFactory @@ -84,13 +85,13 @@ abstract class DatabaseVersioned< protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray @Throws(IOException::class) - fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { - masterKey = getMasterKey(key, keyInputStream) + fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) { + masterKey = getMasterKey(key, keyfileInputStream) } @Throws(IOException::class) - protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray { - val fileKey = getFileKey(keyInputStream) + protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray { + val fileKey = getFileKey(keyfileInputStream) val passwordKey = getPasswordKey(key) val messageDigest: MessageDigest @@ -383,6 +384,12 @@ abstract class DatabaseVersioned< return true } + /** + * Cipher key generated when the database is loaded, and destroyed when the database is closed + * Can be used to temporarily store database elements + */ + var loadedCipherKey: Database.LoadedKey? = null + companion object { private const val TAG = "DatabaseVersioned" diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt index e4e58e3f4..f6d877322 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt @@ -316,7 +316,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte var size = 0L for ((label, poolId) in binaries) { size += label.length.toLong() - size += binaryPool[poolId]?.length() ?: 0 + size += binaryPool[poolId]?.length ?: 0 } return size } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt index ad819cde5..147b7ce74 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.database.file.input +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.tasks.ProgressTaskUpdater @@ -37,10 +38,12 @@ abstract class DatabaseInput> * * @throws LoadDatabaseException on database error (contains IO exceptions) */ + @Throws(LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, password: String?, - keyInputStream: InputStream?, + keyfileInputStream: InputStream?, + loadedCipherKey: Database.LoadedKey, progressTaskUpdater: ProgressTaskUpdater?, fixDuplicateUUID: Boolean = false): PwDb @@ -48,6 +51,7 @@ abstract class DatabaseInput> @Throws(LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, masterKey: ByteArray, + loadedCipherKey: Database.LoadedKey, progressTaskUpdater: ProgressTaskUpdater?, fixDuplicateUUID: Boolean = false): PwDb } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt index f2b67e788..c8664d5eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.file.input import com.kunzisoft.keepass.R import com.kunzisoft.keepass.crypto.CipherFactory +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.group.GroupKDB @@ -48,26 +49,30 @@ import javax.crypto.spec.SecretKeySpec class DatabaseInputKDB(cacheDirectory: File) : DatabaseInput(cacheDirectory) { - private lateinit var mDatabaseToOpen: DatabaseKDB + private lateinit var mDatabase: DatabaseKDB @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, password: String?, - keyInputStream: InputStream?, + keyfileInputStream: InputStream?, + loadedCipherKey: Database.LoadedKey, progressTaskUpdater: ProgressTaskUpdater?, fixDuplicateUUID: Boolean): DatabaseKDB { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { - mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) + mDatabase.loadedCipherKey = loadedCipherKey + mDatabase.retrieveMasterKey(password, keyfileInputStream) } } @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, masterKey: ByteArray, + loadedCipherKey: Database.LoadedKey, progressTaskUpdater: ProgressTaskUpdater?, fixDuplicateUUID: Boolean): DatabaseKDB { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { - mDatabaseToOpen.masterKey = masterKey + mDatabase.loadedCipherKey = loadedCipherKey + mDatabase.masterKey = masterKey } } @@ -101,38 +106,38 @@ class DatabaseInputKDB(cacheDirectory: File) } progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) - mDatabaseToOpen = DatabaseKDB() + mDatabase = DatabaseKDB() - mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID + mDatabase.changeDuplicateId = fixDuplicateUUID assignMasterKey?.invoke() // Select algorithm when { header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt() != 0 -> { - mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael + mDatabase.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael } header.flags.toKotlinInt() and DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt() != 0 -> { - mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish + mDatabase.encryptionAlgorithm = EncryptionAlgorithm.Twofish } else -> throw InvalidAlgorithmDatabaseException() } - mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong() + mDatabase.numberKeyEncryptionRounds = header.numKeyEncRounds.toKotlinLong() // Generate transformedMasterKey from masterKey - mDatabaseToOpen.makeFinalKey( + mDatabase.makeFinalKey( header.masterSeed, header.transformSeed, - mDatabaseToOpen.numberKeyEncryptionRounds) + mDatabase.numberKeyEncryptionRounds) progressTaskUpdater?.updateMessage(R.string.decrypting_db) // Initialize Rijndael algorithm val cipher: Cipher = try { when { - mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> { + mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> { CipherFactory.getInstance("AES/CBC/PKCS5Padding") } - mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> { + mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> { CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING") } else -> throw IOException("Encryption algorithm is not supported") @@ -145,7 +150,7 @@ class DatabaseInputKDB(cacheDirectory: File) try { cipher.init(Cipher.DECRYPT_MODE, - SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), + SecretKeySpec(mDatabase.finalKey, "AES"), IvParameterSpec(header.encryptionIV)) } catch (e1: InvalidKeyException) { throw IOException("Invalid key") @@ -169,9 +174,9 @@ class DatabaseInputKDB(cacheDirectory: File) ) // New manual root because KDB contains multiple root groups (here available with getRootGroups()) - val newRoot = mDatabaseToOpen.createGroup() + val newRoot = mDatabase.createGroup() newRoot.level = -1 - mDatabaseToOpen.rootGroup = newRoot + mDatabase.rootGroup = newRoot // Import all nodes var newGroup: GroupKDB? = null @@ -192,12 +197,12 @@ class DatabaseInputKDB(cacheDirectory: File) // Create new node depending on byte number when (fieldSize) { 4 -> { - newGroup = mDatabaseToOpen.createGroup().apply { + newGroup = mDatabase.createGroup().apply { setGroupId(cipherInputStream.readBytes4ToUInt().toKotlinInt()) } } 16 -> { - newEntry = mDatabaseToOpen.createEntry().apply { + newEntry = mDatabase.createEntry().apply { nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid()) } } @@ -211,7 +216,7 @@ class DatabaseInputKDB(cacheDirectory: File) group.title = cipherInputStream.readBytesToString(fieldSize) } ?: newEntry?.let { entry -> - val groupKDB = mDatabaseToOpen.createGroup() + val groupKDB = mDatabase.createGroup() groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToUInt().toKotlinInt()) entry.parent = groupKDB } @@ -226,7 +231,7 @@ class DatabaseInputKDB(cacheDirectory: File) if (iconId == -1) { iconId = 0 } - entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId) + entry.icon = mDatabase.iconFactory.getIcon(iconId) } } 0x0004 -> { @@ -255,7 +260,7 @@ class DatabaseInputKDB(cacheDirectory: File) } 0x0007 -> { newGroup?.let { group -> - group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) + group.icon = mDatabase.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) } ?: newEntry?.let { entry -> entry.password = cipherInputStream.readBytesToString(fieldSize,false) @@ -300,11 +305,12 @@ class DatabaseInputKDB(cacheDirectory: File) 0x000E -> { newEntry?.let { entry -> if (fieldSize > 0) { - val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory) + val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory) entry.binaryData = binaryAttachment - BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream -> - cipherInputStream.readBytes(fieldSize, - DatabaseKDB.BUFFER_SIZE_BYTES) { buffer -> + val cipherKey = mDatabase.loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to load binaries") + BufferedOutputStream(binaryAttachment.getOutputDataStream(cipherKey)).use { outputStream -> + cipherInputStream.readBytes(fieldSize) { buffer -> outputStream.write(buffer) } } @@ -314,12 +320,12 @@ class DatabaseInputKDB(cacheDirectory: File) 0xFFFF -> { // End record. Save node and count it. newGroup?.let { group -> - mDatabaseToOpen.addGroupIndex(group) + mDatabase.addGroupIndex(group) currentGroupNumber++ newGroup = null } newEntry?.let { entry -> - mDatabaseToOpen.addEntryIndex(entry) + mDatabase.addEntryIndex(entry) currentEntryNumber++ newEntry = null } @@ -337,20 +343,20 @@ class DatabaseInputKDB(cacheDirectory: File) constructTreeFromIndex() } catch (e: LoadDatabaseException) { - mDatabaseToOpen.clearCache() + mDatabase.clearCache() throw e } catch (e: IOException) { - mDatabaseToOpen.clearCache() + mDatabase.clearCache() throw IODatabaseException(e) } catch (e: OutOfMemoryError) { - mDatabaseToOpen.clearCache() + mDatabase.clearCache() throw NoMemoryDatabaseException(e) } catch (e: Exception) { - mDatabaseToOpen.clearCache() + mDatabase.clearCache() throw LoadDatabaseException(e) } - return mDatabaseToOpen + return mDatabase } private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator) { @@ -375,18 +381,18 @@ class DatabaseInputKDB(cacheDirectory: File) } private fun constructTreeFromIndex() { - mDatabaseToOpen.rootGroup?.let { + mDatabase.rootGroup?.let { // add each group - val groupIterator = mDatabaseToOpen.getGroupIndexes().iterator() + val groupIterator = mDatabase.getGroupIndexes().iterator() if (groupIterator.hasNext()) buildTreeGroups(it, groupIterator.next(), groupIterator) // add each child - for (currentEntry in mDatabaseToOpen.getEntryIndexes()) { + for (currentEntry in mDatabase.getEntryIndexes()) { if (currentEntry.parent != null) { // Only the parent id is known so complete the info - val parentGroupRetrieve = mDatabaseToOpen.getGroupById(currentEntry.parent!!.nodeId) + val parentGroupRetrieve = mDatabase.getGroupById(currentEntry.parent!!.nodeId) parentGroupRetrieve?.addChildEntry(currentEntry) currentEntry.parent = parentGroupRetrieve } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt index 14276b2a9..185af9ba6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt @@ -26,6 +26,7 @@ import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.database.BinaryAttachment @@ -96,20 +97,24 @@ class DatabaseInputKDBX(cacheDirectory: File) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, password: String?, - keyInputStream: InputStream?, + keyfileInputStream: InputStream?, + loadedCipherKey: Database.LoadedKey, progressTaskUpdater: ProgressTaskUpdater?, fixDuplicateUUID: Boolean): DatabaseKDBX { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { - mDatabase.retrieveMasterKey(password, keyInputStream) + mDatabase.loadedCipherKey = loadedCipherKey + mDatabase.retrieveMasterKey(password, keyfileInputStream) } } @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, masterKey: ByteArray, + loadedCipherKey: Database.LoadedKey, progressTaskUpdater: ProgressTaskUpdater?, fixDuplicateUUID: Boolean): DatabaseKDBX { return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { + mDatabase.loadedCipherKey = loadedCipherKey mDatabase.masterKey = masterKey } } @@ -273,8 +278,10 @@ class DatabaseInputKDBX(cacheDirectory: File) val byteLength = size - 1 // No compression at this level val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag) - protectedBinary.getOutputDataStream().use { outputStream -> - dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer -> + val cipherKey = mDatabase.loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to load binaries") + protectedBinary.getOutputDataStream(cipherKey).use { outputStream -> + dataInputStream.readBytes(byteLength) { buffer -> outputStream.write(buffer) } } @@ -1009,14 +1016,16 @@ class DatabaseInputKDBX(cacheDirectory: File) // Build the new binary and compress val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId) + val binaryCipherKey = mDatabase.loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to load binaries") try { - binaryAttachment.getOutputDataStream().use { outputStream -> + binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream -> outputStream.write(Base64.decode(base64, BASE_64_FLAG)) } } catch (e: Exception) { Log.e(TAG, "Unable to read base 64 attachment", e) binaryAttachment.isCorrupted = true - binaryAttachment.getOutputDataStream().use { outputStream -> + binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream -> outputStream.write(base64.toByteArray()) } } @@ -1083,6 +1092,7 @@ class DatabaseInputKDBX(cacheDirectory: File) return xpp } } + } @Throws(IOException::class, XmlPullParserException::class) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt index 417865c21..6c145529f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt @@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.file.output import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.database.element.database.DatabaseKDB -import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.group.GroupKDB +import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB @@ -197,15 +197,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, @Suppress("CAST_NEVER_SUCCEEDS") @Throws(DatabaseOutputException::class) - fun outputPlanGroupAndEntries(os: OutputStream) { - val los = LittleEndianDataOutputStream(os) + fun outputPlanGroupAndEntries(outputStream: OutputStream) { + val littleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream) // useHeaderHash if (headerHashBlock != null) { try { - los.writeUShort(0x0000) - los.writeInt(headerHashBlock!!.size) - los.write(headerHashBlock!!) + littleEndianDataOutputStream.writeUShort(0x0000) + littleEndianDataOutputStream.writeInt(headerHashBlock!!.size) + littleEndianDataOutputStream.write(headerHashBlock!!) } catch (e: IOException) { throw DatabaseOutputException("Failed to output header hash.", e) } @@ -213,20 +213,13 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, // Groups mDatabaseKDB.doForEachGroupInIndex { group -> - val pgo = GroupOutputKDB(group, os) - try { - pgo.output() - } catch (e: IOException) { - throw DatabaseOutputException("Failed to output a tree", e) - } + GroupOutputKDB(group, outputStream).output() } + // Entries + val binaryCipherKey = mDatabaseKDB.loadedCipherKey + ?: throw DatabaseOutputException("Unable to retrieve cipher key to write binaries") mDatabaseKDB.doForEachEntryInIndex { entry -> - val peo = EntryOutputKDB(entry, os) - try { - peo.output() - } catch (e: IOException) { - throw DatabaseOutputException("Failed to output an entry.", e) - } + EntryOutputKDB(entry, outputStream, binaryCipherKey).output() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt index 9fb0f0d37..27a98c388 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt @@ -32,7 +32,6 @@ import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG -import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX @@ -140,12 +139,14 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, database.binaryPool.doForEachOrderedBinary { _, keyBinary -> val protectedBinary = keyBinary.binary + val binaryCipherKey = database.loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to write binaries") // Force decompression to add binary in header - protectedBinary.decompress() + protectedBinary.decompress(binaryCipherKey) // Write type binary dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) // Write size - dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1)) + dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length + 1)) // Write protected flag var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None if (protectedBinary.isProtected) { @@ -153,8 +154,8 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } dataOutputStream.writeByte(flag) - protectedBinary.getInputDataStream().use { inputStream -> - inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + protectedBinary.getInputDataStream(binaryCipherKey).use { inputStream -> + inputStream.readAllBytes { buffer -> dataOutputStream.write(buffer) } } @@ -473,7 +474,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, if (binary.isProtected) { xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) binary.getInputDataStream().use { inputStream -> - inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + inputStream.readBytes { buffer -> val encoded = ByteArray(buffer.size) randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0) xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) @@ -482,7 +483,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } else { // Write the XML binary.getInputDataStream().use { inputStream -> - inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + inputStream.readBytes { buffer -> xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) } } @@ -502,13 +503,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, xml.startTag(null, DatabaseKDBXXML.ElemBinary) xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) val binary = keyBinary.binary - if (binary.length() > 0) { + if (binary.length > 0) { if (binary.isCompressed) { xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) } // Write the XML - binary.getInputDataStream().use { inputStream -> - inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + val binaryCipherKey = mDatabaseKDBX.loadedCipherKey + ?: throw IOException("Unable to retrieve cipher key to write binaries") + binary.getInputDataStream(binaryCipherKey).use { inputStream -> + inputStream.readAllBytes { buffer -> xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) } } @@ -589,7 +592,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) } } else { - xml.text(safeXmlString(value.toString())) + xml.text(value.toString()) } xml.endTag(null, DatabaseKDBXXML.ElemValue) @@ -718,17 +721,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, if (text.isEmpty()) { return text } - val stringBuilder = StringBuilder() - var ch: Char + var character: Char for (element in text) { - ch = element + character = element + val hexChar = character.toInt() if ( - ch.toInt() in 0x20..0xD7FF || - ch.toInt() == 0x9 || ch.toInt() == 0xA || ch.toInt() == 0xD || - ch.toInt() in 0xE000..0xFFFD + hexChar in 0x20..0xD7FF || + hexChar == 0x9 || + hexChar == 0xA || + hexChar == 0xD || + hexChar in 0xE000..0xFFFD ) { - stringBuilder.append(ch) + stringBuilder.append(character) } } return stringBuilder.toString() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt index e06f6eb9d..25461e985 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/EntryOutputKDB.kt @@ -19,9 +19,9 @@ */ package com.kunzisoft.keepass.database.file.output -import com.kunzisoft.keepass.database.element.database.DatabaseKDB +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.entry.EntryKDB -import com.kunzisoft.keepass.database.file.output.GroupOutputKDB.Companion.GROUPID_FIELD_SIZE +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils import com.kunzisoft.keepass.utils.UnsignedInt @@ -29,96 +29,91 @@ import java.io.IOException import java.io.OutputStream import java.nio.charset.Charset -class EntryOutputKDB /** * Output the GroupKDB to the stream */ -(private val mEntry: EntryKDB, private val mOutputStream: OutputStream) { - /** - * Returns the number of bytes written by the stream - * @return Number of bytes written - */ - var length: Long = 0 - private set +class EntryOutputKDB(private val mEntry: EntryKDB, + private val mOutputStream: OutputStream, + private val mCipherKey: Database.LoadedKey) { //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int - @Throws(IOException::class) + @Throws(DatabaseOutputException::class) fun output() { + try { + // UUID + mOutputStream.write(UUID_FIELD_TYPE) + mOutputStream.write(UUID_FIELD_SIZE) + mOutputStream.write(uuidTo16Bytes(mEntry.id)) - length += 134 // Length of fixed size fields + // Group ID + mOutputStream.write(GROUPID_FIELD_TYPE) + mOutputStream.write(GROUPID_FIELD_SIZE) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id))) - // UUID - mOutputStream.write(UUID_FIELD_TYPE) - mOutputStream.write(UUID_FIELD_SIZE) - mOutputStream.write(uuidTo16Bytes(mEntry.id)) + // Image ID + mOutputStream.write(IMAGEID_FIELD_TYPE) + mOutputStream.write(IMAGEID_FIELD_SIZE) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId))) - // Group ID - mOutputStream.write(GROUPID_FIELD_TYPE) - mOutputStream.write(GROUPID_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.parent!!.id))) + // Title + //byte[] title = mEntry.title.getBytes("UTF-8"); + mOutputStream.write(TITLE_FIELD_TYPE) + StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.title) - // Image ID - mOutputStream.write(IMAGEID_FIELD_TYPE) - mOutputStream.write(IMAGEID_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId))) + // URL + mOutputStream.write(URL_FIELD_TYPE) + StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.url) - // Title - //byte[] title = mEntry.title.getBytes("UTF-8"); - mOutputStream.write(TITLE_FIELD_TYPE) - length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.title, mOutputStream).toLong() + // Username + mOutputStream.write(USERNAME_FIELD_TYPE) + StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.username) - // URL - mOutputStream.write(URL_FIELD_TYPE) - length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.url, mOutputStream).toLong() + // Password + mOutputStream.write(PASSWORD_FIELD_TYPE) + writePassword(mEntry.password, mOutputStream) - // Username - mOutputStream.write(USERNAME_FIELD_TYPE) - length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.username, mOutputStream).toLong() + // Additional + mOutputStream.write(ADDITIONAL_FIELD_TYPE) + StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.notes) - // Password - mOutputStream.write(PASSWORD_FIELD_TYPE) - length += writePassword(mEntry.password, mOutputStream).toLong() + // Create date + writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date)) - // Additional - mOutputStream.write(ADDITIONAL_FIELD_TYPE) - length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.notes, mOutputStream).toLong() + // Modification date + writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date)) - // Create date - writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date)) + // Access date + writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date)) - // Modification date - writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date)) + // Expiration date + writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date)) - // Access date - writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date)) + // Binary description + mOutputStream.write(BINARY_DESC_FIELD_TYPE) + StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription) - // Expiration date - writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date)) - - // Binary description - mOutputStream.write(BINARY_DESC_FIELD_TYPE) - length += StringDatabaseKDBUtils.writeStringToBytes(mEntry.binaryDescription, mOutputStream).toLong() - - // Binary - mOutputStream.write(BINARY_DATA_FIELD_TYPE) - val binaryData = mEntry.binaryData - val binaryDataLength = binaryData?.length() ?: 0L - // Write data length - mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength))) - // Write data - if (binaryDataLength > 0) { - binaryData?.getInputDataStream().use { inputStream -> - inputStream?.readBytes(DatabaseKDB.BUFFER_SIZE_BYTES) { buffer -> - length += buffer.size - mOutputStream.write(buffer) + // Binary + mOutputStream.write(BINARY_DATA_FIELD_TYPE) + val binaryData = mEntry.binaryData + val binaryDataLength = binaryData?.length ?: 0L + // Write data length + mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength))) + // Write data + if (binaryDataLength > 0) { + binaryData?.getInputDataStream(mCipherKey).use { inputStream -> + inputStream?.readAllBytes { buffer -> + mOutputStream.write(buffer) + } + inputStream?.close() } - inputStream?.close() } - } - // End - mOutputStream.write(END_FIELD_TYPE) - mOutputStream.write(ZERO_FIELD_SIZE) + // End + mOutputStream.write(END_FIELD_TYPE) + mOutputStream.write(ZERO_FIELD_SIZE) + } catch (e: IOException) { + throw DatabaseOutputException("Failed to output an entry.", e) + } } @Throws(IOException::class) @@ -144,29 +139,27 @@ class EntryOutputKDB companion object { // Constants - val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1) - val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) - val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) - val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) - val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) - val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) - val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) - val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) - val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) - val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10) - val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11) - val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12) - val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13) - val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14) - val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) + private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1) + private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) + private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) + private val TITLE_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) + private val URL_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) + private val USERNAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) + private val PASSWORD_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) + private val ADDITIONAL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) + private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) + private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(10) + private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(11) + private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(12) + private val BINARY_DESC_FIELD_TYPE:ByteArray = uShortTo2Bytes(13) + private val BINARY_DATA_FIELD_TYPE:ByteArray = uShortTo2Bytes(14) + private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) - val LONG_FOUR:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16)) - val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) - val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) - val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00) + private val UUID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(16)) + private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) + private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) + private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) + private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) + private val ZERO_FIVE:ByteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt index d1e157fa3..326dcf543 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/GroupOutputKDB.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.database.file.output import com.kunzisoft.keepass.database.element.group.GroupKDB +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.stream.dateTo5Bytes import com.kunzisoft.keepass.stream.uIntTo4Bytes import com.kunzisoft.keepass.stream.uShortTo2Bytes @@ -31,79 +32,84 @@ import java.io.OutputStream /** * Output the GroupKDB to the stream */ -class GroupOutputKDB (private val mGroup: GroupKDB, private val mOutputStream: OutputStream) { +class GroupOutputKDB(private val mGroup: GroupKDB, + private val mOutputStream: OutputStream) { - @Throws(IOException::class) + @Throws(DatabaseOutputException::class) fun output() { - //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter. + try { + //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int, but most values can't be greater than 2^31, so it probably doesn't matter. - // Group ID - mOutputStream.write(GROUPID_FIELD_TYPE) - mOutputStream.write(GROUPID_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id))) + // Group ID + mOutputStream.write(GROUPID_FIELD_TYPE) + mOutputStream.write(GROUPID_FIELD_SIZE) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.id))) - // Name - mOutputStream.write(NAME_FIELD_TYPE) - StringDatabaseKDBUtils.writeStringToBytes(mGroup.title, mOutputStream) + // Name + mOutputStream.write(NAME_FIELD_TYPE) + StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mGroup.title) - // Create date - mOutputStream.write(CREATE_FIELD_TYPE) - mOutputStream.write(DATE_FIELD_SIZE) - mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date)) + // Create date + mOutputStream.write(CREATE_FIELD_TYPE) + mOutputStream.write(DATE_FIELD_SIZE) + mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date)) - // Modification date - mOutputStream.write(MOD_FIELD_TYPE) - mOutputStream.write(DATE_FIELD_SIZE) - mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date)) + // Modification date + mOutputStream.write(MOD_FIELD_TYPE) + mOutputStream.write(DATE_FIELD_SIZE) + mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date)) - // Access date - mOutputStream.write(ACCESS_FIELD_TYPE) - mOutputStream.write(DATE_FIELD_SIZE) - mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date)) + // Access date + mOutputStream.write(ACCESS_FIELD_TYPE) + mOutputStream.write(DATE_FIELD_SIZE) + mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date)) - // Expiration date - mOutputStream.write(EXPIRE_FIELD_TYPE) - mOutputStream.write(DATE_FIELD_SIZE) - mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date)) + // Expiration date + mOutputStream.write(EXPIRE_FIELD_TYPE) + mOutputStream.write(DATE_FIELD_SIZE) + mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date)) - // Image ID - mOutputStream.write(IMAGEID_FIELD_TYPE) - mOutputStream.write(IMAGEID_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId))) + // Image ID + mOutputStream.write(IMAGEID_FIELD_TYPE) + mOutputStream.write(IMAGEID_FIELD_SIZE) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId))) - // Level - mOutputStream.write(LEVEL_FIELD_TYPE) - mOutputStream.write(LEVEL_FIELD_SIZE) - mOutputStream.write(uShortTo2Bytes(mGroup.level)) + // Level + mOutputStream.write(LEVEL_FIELD_TYPE) + mOutputStream.write(LEVEL_FIELD_SIZE) + mOutputStream.write(uShortTo2Bytes(mGroup.level)) - // Flags - mOutputStream.write(FLAGS_FIELD_TYPE) - mOutputStream.write(FLAGS_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags))) + // Flags + mOutputStream.write(FLAGS_FIELD_TYPE) + mOutputStream.write(FLAGS_FIELD_SIZE) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.groupFlags))) - // End - mOutputStream.write(END_FIELD_TYPE) - mOutputStream.write(ZERO_FIELD_SIZE) + // End + mOutputStream.write(END_FIELD_TYPE) + mOutputStream.write(ZERO_FIELD_SIZE) + } catch (e: IOException) { + throw DatabaseOutputException("Failed to output a group.", e) + } } companion object { // Constants - val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1) - val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) - val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) - val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) - val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) - val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) - val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) - val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) - val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) - val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) + private val GROUPID_FIELD_TYPE: ByteArray = uShortTo2Bytes(1) + private val NAME_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) + private val CREATE_FIELD_TYPE:ByteArray = uShortTo2Bytes(3) + private val MOD_FIELD_TYPE:ByteArray = uShortTo2Bytes(4) + private val ACCESS_FIELD_TYPE:ByteArray = uShortTo2Bytes(5) + private val EXPIRE_FIELD_TYPE:ByteArray = uShortTo2Bytes(6) + private val IMAGEID_FIELD_TYPE:ByteArray = uShortTo2Bytes(7) + private val LEVEL_FIELD_TYPE:ByteArray = uShortTo2Bytes(8) + private val FLAGS_FIELD_TYPE:ByteArray = uShortTo2Bytes(9) + private val END_FIELD_TYPE:ByteArray = uShortTo2Bytes(0xFFFF) - val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) - val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2)) - val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) - val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) + private val GROUPID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) + private val DATE_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(5)) + private val IMAGEID_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) + private val LEVEL_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(2)) + private val FLAGS_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(4)) + private val ZERO_FIELD_SIZE:ByteArray = uIntTo4Bytes(UnsignedInt(0)) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt index 553641f20..5dc9730be 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt @@ -41,7 +41,7 @@ import com.kunzisoft.keepass.adapters.FieldsAdapter import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.Field -import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService +import com.kunzisoft.keepass.services.KeyboardEntryNotificationService import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.* diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt index 02257d9ba..26b9ed5e4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt @@ -29,19 +29,22 @@ import com.kunzisoft.keepass.utils.writeEnum data class EntryAttachmentState(var attachment: Attachment, var streamDirection: StreamDirection, var downloadState: AttachmentState = AttachmentState.NULL, - var downloadProgression: Int = 0) : Parcelable { + var downloadProgression: Int = 0, + var previewState: AttachmentState = AttachmentState.NULL) : Parcelable { constructor(parcel: Parcel) : this( parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()), parcel.readEnum() ?: StreamDirection.DOWNLOAD, parcel.readEnum() ?: AttachmentState.NULL, - parcel.readInt()) + parcel.readInt(), + parcel.readEnum() ?: AttachmentState.NULL) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeParcelable(attachment, flags) parcel.writeEnum(streamDirection) parcel.writeEnum(downloadState) parcel.writeInt(downloadProgression) + parcel.writeEnum(previewState) } override fun describeContents(): Int { @@ -73,5 +76,5 @@ data class EntryAttachmentState(var attachment: Attachment, } enum class AttachmentState { - NULL, START, IN_PROGRESS, COMPLETE, ERROR + NULL, START, IN_PROGRESS, COMPLETE, CANCELED, ERROR } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 73f2ab8f2..32ff5b3e9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -23,44 +23,28 @@ import android.os.Parcel import android.os.Parcelable import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.DateInstant -import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD -import kotlin.collections.ArrayList -class EntryInfo : Parcelable { +class EntryInfo : NodeInfo { var id: String = "" - var title: String = "" - var icon: IconImage = IconImageStandard() var username: String = "" var password: String = "" - var creationTime: DateInstant = DateInstant() - var modificationTime: DateInstant = DateInstant() - var expires: Boolean = false - var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH var url: String = "" var notes: String = "" var customFields: List = ArrayList() var attachments: List = ArrayList() var otpModel: OtpModel? = null - constructor() + constructor(): super() - private constructor(parcel: Parcel) { + constructor(parcel: Parcel): super(parcel) { id = parcel.readString() ?: id - title = parcel.readString() ?: title - icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon username = parcel.readString() ?: username password = parcel.readString() ?: password - creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime - modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime - expires = parcel.readInt() != 0 - expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime url = parcel.readString() ?: url notes = parcel.readString() ?: notes parcel.readList(customFields, Field::class.java.classLoader) @@ -73,15 +57,10 @@ class EntryInfo : Parcelable { } override fun writeToParcel(parcel: Parcel, flags: Int) { + super.writeToParcel(parcel, flags) parcel.writeString(id) - parcel.writeString(title) - parcel.writeParcelable(icon, flags) parcel.writeString(username) parcel.writeString(password) - parcel.writeParcelable(creationTime, flags) - parcel.writeParcelable(modificationTime, flags) - parcel.writeInt(if (expires) 1 else 0) - parcel.writeParcelable(expiryTime, flags) parcel.writeString(url) parcel.writeString(notes) parcel.writeArray(customFields.toTypedArray()) diff --git a/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt new file mode 100644 index 000000000..9c4890ac8 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt @@ -0,0 +1,36 @@ +package com.kunzisoft.keepass.model + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.database.element.icon.IconImageStandard +import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER + +class GroupInfo : NodeInfo { + + var notes: String? = null + + init { + icon = IconImageStandard(FOLDER) + } + + constructor(): super() + + constructor(parcel: Parcel): super(parcel) { + notes = parcel.readString() + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + super.writeToParcel(parcel, flags) + parcel.writeString(notes) + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): GroupInfo { + return GroupInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt new file mode 100644 index 000000000..e5ee12c72 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt @@ -0,0 +1,32 @@ +package com.kunzisoft.keepass.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readParcelable(Uri::class.java.classLoader)) { + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(masterPassword) + parcel.writeParcelable(keyFileUri, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MainCredential { + return MainCredential(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt new file mode 100644 index 000000000..9c2a1d252 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt @@ -0,0 +1,49 @@ +package com.kunzisoft.keepass.model + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.icon.IconImage +import com.kunzisoft.keepass.database.element.icon.IconImageStandard + +open class NodeInfo() : Parcelable { + + var title: String = "" + var icon: IconImage = IconImageStandard() + var creationTime: DateInstant = DateInstant() + var lastModificationTime: DateInstant = DateInstant() + var expires: Boolean = false + var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH + + constructor(parcel: Parcel) : this() { + title = parcel.readString() ?: title + icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon + creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime + lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: lastModificationTime + expires = parcel.readInt() != 0 + expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(title) + parcel.writeParcelable(icon, flags) + parcel.writeParcelable(creationTime, flags) + parcel.writeParcelable(lastModificationTime, flags) + parcel.writeInt(if (expires) 1 else 0) + parcel.writeParcelable(expiryTime, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): NodeInfo { + return NodeInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt index bddb8a284..5244fac28 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt @@ -210,13 +210,13 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) { fun isValidBase32(secret: String): Boolean { val secretChars = replaceBase32Chars(secret) return secret.isNotEmpty() - && (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secretChars)) + && (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}=*|[A-Z2-7]{4}=*|[A-Z2-7]{5}=*|[A-Z2-7]{7}=*)?$", secretChars)) } fun isValidBase64(secret: String): Boolean { // TODO replace base 64 chars return secret.isNotEmpty() - && (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret)) + && (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}=*|[A-Za-z0-9+/]{3}=*)?$", secret)) } fun replaceBase32Chars(parameter: String): String { diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt index 92121640c..9e382b39a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt @@ -354,9 +354,15 @@ object OtpEntryFields { return false } otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD - otpElement.tokenType = matcher.group(2)?.let { - OtpTokenType.getFromString(it) - } ?: OtpTokenType.RFC6238 + matcher.group(2)?.let { secondMatcher -> + try { + otpElement.digits = secondMatcher.toInt() + } catch (e: NumberFormatException) { + otpElement.digits = OTP_DEFAULT_DIGITS + otpElement.tokenType = OtpTokenType.getFromString(secondMatcher) + } + } + } } catch (exception: Exception) { return false diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt index 7974e24bb..26c56d01f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt @@ -1,4 +1,4 @@ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Context diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt similarity index 75% rename from app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt index 341f78f22..adfd276ca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.ContentResolver @@ -29,15 +29,14 @@ import android.util.Log import androidx.documentfile.provider.DocumentFile import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.StreamDirection -import com.kunzisoft.keepass.stream.readBytes -import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.stream.readAllBytes import com.kunzisoft.keepass.utils.UriUtil import kotlinx.coroutines.* -import java.io.BufferedInputStream import java.util.* import java.util.concurrent.CopyOnWriteArrayList @@ -100,12 +99,15 @@ class AttachmentFileNotificationService: LockNotificationService() { when(intent?.action) { ACTION_ATTACHMENT_FILE_START_UPLOAD -> { - actionUploadOrDownload(downloadFileUri, + actionStartUploadOrDownload(downloadFileUri, intent, StreamDirection.UPLOAD) } + ACTION_ATTACHMENT_FILE_STOP_UPLOAD -> { + actionStopUpload() + } ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> { - actionUploadOrDownload(downloadFileUri, + actionStartUploadOrDownload(downloadFileUri, intent, StreamDirection.DOWNLOAD) } @@ -215,15 +217,22 @@ class AttachmentFileNotificationService: LockNotificationService() { setDeleteIntent(pendingDeleteIntent) setOngoing(false) } + AttachmentState.CANCELED -> { + setContentText(getString(R.string.download_canceled)) + setDeleteIntent(pendingDeleteIntent) + setOngoing(false) + } AttachmentState.ERROR -> { setContentText(getString(R.string.error_file_not_create)) + setDeleteIntent(pendingDeleteIntent) setOngoing(false) } } } when (attachmentNotification.entryAttachmentState.downloadState) { - AttachmentState.ERROR, - AttachmentState.COMPLETE -> { + AttachmentState.COMPLETE, + AttachmentState.CANCELED, + AttachmentState.ERROR -> { stopForeground(false) notificationManager?.notify(attachmentNotification.notificationId, builder.build()) } else -> { @@ -234,6 +243,7 @@ class AttachmentFileNotificationService: LockNotificationService() { override fun onDestroy() { attachmentNotificationList.forEach { attachmentNotification -> + attachmentNotification.attachmentFileAction?.cancel() attachmentNotification.attachmentFileAction?.listener = null notificationManager?.cancel(attachmentNotification.notificationId) } @@ -262,10 +272,10 @@ class AttachmentFileNotificationService: LockNotificationService() { } } - private fun actionUploadOrDownload(downloadFileUri: Uri?, - intent: Intent, - streamDirection: StreamDirection) { - if (downloadFileUri != null + private fun actionStartUploadOrDownload(fileUri: Uri?, + intent: Intent, + streamDirection: StreamDirection) { + if (fileUri != null && intent.hasExtra(ATTACHMENT_KEY)) { try { intent.getParcelableExtra(ATTACHMENT_KEY)?.let { entryAttachment -> @@ -273,7 +283,7 @@ class AttachmentFileNotificationService: LockNotificationService() { val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId } ?.notificationId ?: notificationId) + 1 val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection) - val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState) + val attachmentNotification = AttachmentNotification(fileUri, nextNotificationId, entryAttachmentState) // Add action to the list on start attachmentNotificationList.add(attachmentNotification) @@ -286,11 +296,24 @@ class AttachmentFileNotificationService: LockNotificationService() { } } } catch (e: Exception) { - Log.e(TAG, "Unable to upload/download $downloadFileUri", e) + Log.e(TAG, "Unable to upload/download $fileUri", e) } } } + private fun actionStopUpload() { + try { + // Stop each upload + attachmentNotificationList.filter { + it.entryAttachmentState.streamDirection == StreamDirection.UPLOAD + }.forEach { + it.attachmentFileAction?.cancel() + } + } catch (e: Exception) { + Log.e(TAG, "Unable to stop upload", e) + } + } + private class AttachmentFileAction( private val attachmentNotification: AttachmentNotification, private val contentResolver: ContentResolver) { @@ -307,8 +330,6 @@ class AttachmentFileNotificationService: LockNotificationService() { // on pre execute CoroutineScope(Dispatchers.Main).launch { - TimeoutHelper.temporarilyDisableTimeout() - attachmentNotification.attachmentFileAction = this@AttachmentFileAction attachmentNotification.entryAttachmentState.apply { downloadState = AttachmentState.START @@ -319,70 +340,81 @@ class AttachmentFileNotificationService: LockNotificationService() { withContext(Dispatchers.IO) { // on Progress with thread - val asyncResult: Deferred = async { - var progressResult = true - try { - attachmentNotification.entryAttachmentState.apply { - downloadState = AttachmentState.IN_PROGRESS + val asyncAction = launch { + attachmentNotification.entryAttachmentState.apply { + try { + downloadState = AttachmentState.IN_PROGRESS - when (streamDirection) { - StreamDirection.UPLOAD -> { - uploadToDatabase( - attachmentNotification.uri, - attachment.binaryAttachment, - contentResolver, 1024) { percent -> - publishProgress(percent) + when (streamDirection) { + StreamDirection.UPLOAD -> { + uploadToDatabase( + attachmentNotification.uri, + attachment.binaryAttachment, + contentResolver, 1024, + { // Cancellation + downloadState == AttachmentState.CANCELED + } + ) { percent -> + publishProgress(percent) + } + } + StreamDirection.DOWNLOAD -> { + downloadFromDatabase( + attachmentNotification.uri, + attachment.binaryAttachment, + contentResolver, 1024) { percent -> + publishProgress(percent) + } } } - StreamDirection.DOWNLOAD -> { - downloadFromDatabase( - attachmentNotification.uri, - attachment.binaryAttachment, - contentResolver, 1024) { percent -> - publishProgress(percent) - } - } - } + } catch (e: Exception) { + e.printStackTrace() + downloadState = AttachmentState.ERROR } - } catch (e: Exception) { - Log.e(TAG, "Unable to upload or download file", e) - progressResult = false } - progressResult + attachmentNotification.entryAttachmentState.downloadState } // on post execute withContext(Dispatchers.Main) { - val result = asyncResult.await() - attachmentNotification.attachmentFileAction = null + asyncAction.join() attachmentNotification.entryAttachmentState.apply { - downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR - downloadProgression = 100 + if (downloadState != AttachmentState.CANCELED + && downloadState != AttachmentState.ERROR) { + downloadState = AttachmentState.COMPLETE + downloadProgression = 100 + } } + attachmentNotification.attachmentFileAction = null listener?.onUpdate(attachmentNotification) - TimeoutHelper.releaseTemporarilyDisableTimeout() } } } + fun cancel() { + attachmentNotification.entryAttachmentState.downloadState = AttachmentState.CANCELED + } + fun downloadFromDatabase(attachmentToUploadUri: Uri, binaryAttachment: BinaryAttachment, contentResolver: ContentResolver, bufferSize: Int = DEFAULT_BUFFER_SIZE, update: ((percent: Int)->Unit)? = null) { var dataDownloaded = 0L - val fileSize = binaryAttachment.length() + val fileSize = binaryAttachment.length UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream -> - binaryAttachment.getUnGzipInputDataStream().use { inputStream -> - inputStream.readBytes(bufferSize) { buffer -> - outputStream.write(buffer) - dataDownloaded += buffer.size - try { - val percentDownload = (100 * dataDownloaded / fileSize).toInt() - update?.invoke(percentDownload) - } catch (e: Exception) { - Log.e(TAG, "", e) + Database.getInstance().loadedCipherKey?.let { binaryCipherKey -> + binaryAttachment.getUnGzipInputDataStream(binaryCipherKey).use { inputStream -> + inputStream.readAllBytes(bufferSize) { buffer -> + outputStream.write(buffer) + dataDownloaded += buffer.size + try { + val percentDownload = (100 * dataDownloaded / fileSize).toInt() + update?.invoke(percentDownload) + } catch (e: Exception) { + Log.e(TAG, "", e) + } } } } @@ -393,13 +425,14 @@ class AttachmentFileNotificationService: LockNotificationService() { binaryAttachment: BinaryAttachment, contentResolver: ContentResolver, bufferSize: Int = DEFAULT_BUFFER_SIZE, + canceled: ()-> Boolean = { false }, update: ((percent: Int)->Unit)? = null) { var dataUploaded = 0L val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0 - UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream -> - binaryAttachment.getGzipOutputDataStream().use { outputStream -> - BufferedInputStream(inputStream).use { attachmentBufferedInputStream -> - attachmentBufferedInputStream.readBytes(bufferSize) { buffer -> + UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream -> + Database.getInstance().loadedCipherKey?.let { binaryCipherKey -> + binaryAttachment.getGzipOutputDataStream(binaryCipherKey).use { outputStream -> + inputStream.readAllBytes(bufferSize, canceled) { buffer -> outputStream.write(buffer) dataUploaded += buffer.size try { @@ -419,7 +452,9 @@ class AttachmentFileNotificationService: LockNotificationService() { val currentTime = System.currentTimeMillis() if (previousSaveTime + updateMinFrequency < currentTime) { attachmentNotification.entryAttachmentState.apply { - downloadState = AttachmentState.IN_PROGRESS + if (downloadState != AttachmentState.CANCELED) { + downloadState = AttachmentState.IN_PROGRESS + } downloadProgression = percent } CoroutineScope(Dispatchers.Main).launch { @@ -441,6 +476,7 @@ class AttachmentFileNotificationService: LockNotificationService() { private const val CHANNEL_ATTACHMENT_ID = "com.kunzisoft.keepass.notification.channel.attachment" const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD" + const val ACTION_ATTACHMENT_FILE_STOP_UPLOAD = "ACTION_ATTACHMENT_FILE_STOP_UPLOAD" const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD" const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE" diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt rename to app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt index 61dfbcfd3..2521db4af 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.os.Parcel import android.os.Parcelable diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt index d0678e7d8..2930f5a78 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Context diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt similarity index 94% rename from app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index cd1cb6efb..14de85eb5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Intent @@ -39,6 +39,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater @@ -141,9 +142,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress val conditionExists = previousDatabaseInfo != null && previousDatabaseInfo.exists != lastFileDatabaseInfo.exists // To prevent dialog opening too often + // Add 10 seconds delta time to prevent spamming val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null && oldDatabaseModification < newDatabaseModification - && mLastLocalSaveTime + 5000 < newDatabaseModification) + && mLastLocalSaveTime + 10000 < newDatabaseModification) if (conditionExists || conditionLastModification) { // Show the dialog only if it's real new info and not a delay after a save @@ -399,10 +401,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress intent?.removeExtra(DATABASE_TASK_WARNING_KEY) intent?.removeExtra(DATABASE_URI_KEY) - intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY) - intent?.removeExtra(MASTER_PASSWORD_KEY) - intent?.removeExtra(KEY_FILE_CHECKED_KEY) - intent?.removeExtra(KEY_FILE_URI_KEY) + intent?.removeExtra(MAIN_CREDENTIAL_KEY) intent?.removeExtra(READ_ONLY_KEY) intent?.removeExtra(CIPHER_ENTITY_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) @@ -474,13 +473,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) - && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) - && intent.hasExtra(MASTER_PASSWORD_KEY) - && intent.hasExtra(KEY_FILE_CHECKED_KEY) - && intent.hasExtra(KEY_FILE_URI_KEY) + && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) - val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY) + val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() if (databaseUri == null) return null @@ -490,14 +486,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress databaseUri, getString(R.string.database_default_name), getString(R.string.database), - intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), - intent.getStringExtra(MASTER_PASSWORD_KEY), - intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), - keyFileUri + mainCredential ) { result -> result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) - putParcelable(KEY_FILE_URI_KEY, keyFileUri) + putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) } } } else { @@ -508,15 +501,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) - && intent.hasExtra(MASTER_PASSWORD_KEY) - && intent.hasExtra(KEY_FILE_URI_KEY) + && intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(READ_ONLY_KEY) && intent.hasExtra(CIPHER_ENTITY_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) - val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY) - val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY) + val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) @@ -527,8 +518,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress this, mDatabase, databaseUri, - masterPassword, - keyFileUri, + mainCredential, readOnly, cipherEntity, intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), @@ -537,8 +527,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress // Add each info to reload database after thrown duplicate UUID exception result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) - putString(MASTER_PASSWORD_KEY, masterPassword) - putParcelable(KEY_FILE_URI_KEY, keyFileUri) + putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(READ_ONLY_KEY, readOnly) putParcelable(CIPHER_ENTITY_KEY, cipherEntity) } @@ -561,19 +550,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? { return if (intent.hasExtra(DATABASE_URI_KEY) - && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) - && intent.hasExtra(MASTER_PASSWORD_KEY) - && intent.hasExtra(KEY_FILE_CHECKED_KEY) - && intent.hasExtra(KEY_FILE_URI_KEY) + && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null AssignPasswordInDatabaseRunnable(this, mDatabase, databaseUri, - intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), - intent.getStringExtra(MASTER_PASSWORD_KEY), - intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), - intent.getParcelableExtra(KEY_FILE_URI_KEY) + intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() ) } else { null @@ -896,10 +879,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY" const val DATABASE_URI_KEY = "DATABASE_URI_KEY" - const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" - const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY" - const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY" - const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY" + const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY" const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt index 1659bb23d..af214e50b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Context diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt similarity index 97% rename from app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt index 2bb4889ec..daf37a124 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.content.Intent import com.kunzisoft.keepass.utils.LockReceiver diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt similarity index 98% rename from app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt index 48b7106c0..e475ad4eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt @@ -1,4 +1,4 @@ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.NotificationChannel import android.app.NotificationManager diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt index 2c4fe2f3e..b9ebbab67 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt @@ -44,7 +44,7 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.icons.IconPackChooser -import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService +import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.settings.preference.IconPackListPreference import com.kunzisoft.keepass.utils.UriUtil diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt index f5a50c05a..fe9a26dc9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt @@ -35,7 +35,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.tasks.ActionRunnable diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt index 7c6e2671a..4760e86c2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -36,9 +36,10 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.view.showActionError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded open class SettingsActivity : LockingActivity(), @@ -98,18 +99,23 @@ open class SettingsActivity when (actionTask) { DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { // Reload the current activity - startActivity(intent) - finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + if (result.isSuccess) { + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } else { + this.showActionErrorIfNeeded(result) + finish() + } } else -> { // Call result in fragment (supportFragmentManager .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) ?.onProgressDialogThreadResult(actionTask, result) - coordinatorLayout?.showActionError(result) } } + coordinatorLayout?.showActionErrorIfNeeded(result) } // To reload the current screen @@ -136,52 +142,33 @@ open class SettingsActivity } override fun onPasswordEncodingValidateListener(databaseUri: Uri?, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + mainCredential: MainCredential) { databaseUri?.let { mProgressDatabaseTaskProvider?.startDatabaseAssignPassword( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } } - override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { Database.getInstance().let { database -> database.fileUri?.let { databaseUri -> // Show the progress dialog now or after dialog confirmation - if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) { + if (database.validatePasswordEncoding(mainCredential)) { mProgressDatabaseTaskProvider?.startDatabaseAssignPassword( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } else { - PasswordEncodingDialogFragment.getInstance(databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile - ).show(supportFragmentManager, "passwordEncodingTag") + PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential) + .show(supportFragmentManager, "passwordEncodingTag") } } } } - override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) {} + override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) { if (PreferencesUtil.showLockDatabaseButton(this)) { diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt b/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt index 4e01ecf3a..e8ede8248 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt +++ b/app/src/main/java/com/kunzisoft/keepass/stream/StreamBytesUtils.kt @@ -30,10 +30,12 @@ import java.util.* * Read all data of stream and invoke [readBytes] each time the buffer is full or no more data to read. */ @Throws(IOException::class) -fun InputStream.readBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) { +fun InputStream.readAllBytes(bufferSize: Int = DEFAULT_BUFFER_SIZE, + cancelCondition: ()-> Boolean = { false }, + readBytes: (bytesRead: ByteArray) -> Unit) { val buffer = ByteArray(bufferSize) var read = 0 - while (read != -1) { + while (read != -1 && !cancelCondition()) { read = this.read(buffer, 0, buffer.size) if (read != -1) { val optimizedBuffer: ByteArray = if (buffer.size == read) { @@ -50,7 +52,8 @@ fun InputStream.readBytes(bufferSize: Int, readBytes: (bytesRead: ByteArray) -> * Read number of bytes defined by [length] and invoke [readBytes] each time the buffer is full or no more data to read. */ @Throws(IOException::class) -fun InputStream.readBytes(length: Int, bufferSize: Int, readBytes: (bytesRead: ByteArray) -> Unit) { +fun InputStream.readBytes(length: Int, bufferSize: Int = DEFAULT_BUFFER_SIZE, + readBytes: (bytesRead: ByteArray) -> Unit) { var bufferLength = bufferSize var buffer = ByteArray(bufferLength) diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt index b071c33d6..933a47536 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt @@ -31,10 +31,11 @@ import androidx.fragment.app.FragmentActivity import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE +import com.kunzisoft.keepass.services.AttachmentFileNotificationService +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_STOP_UPLOAD +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE class AttachmentFileBinderManager(private val activity: FragmentActivity) { @@ -120,6 +121,10 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) { }, ACTION_ATTACHMENT_FILE_START_UPLOAD) } + fun stopUploadAllAttachments() { + start(null, ACTION_ATTACHMENT_FILE_STOP_UPLOAD) + } + fun startDownloadAttachment(downloadFileUri: Uri, attachment: Attachment) { start(Bundle().apply { diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt index 3dcfcf7b4..c12fb0838 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt @@ -32,8 +32,8 @@ import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.magikeyboard.MagikIME -import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService -import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService +import com.kunzisoft.keepass.services.ClipboardEntryNotificationService +import com.kunzisoft.keepass.services.KeyboardEntryNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt b/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt index 611e97b34..229487c8c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/StringDatabaseKDBUtils.kt @@ -53,12 +53,12 @@ object StringDatabaseKDBUtils { } @Throws(IOException::class) - fun writeStringToBytes(string: String?, os: OutputStream): Int { + fun writeStringToStream(outputStream: OutputStream, string: String?): Int { var str = string if (str == null) { // Write out a null character - os.write(uIntTo4Bytes(UnsignedInt(1))) - os.write(0x00) + outputStream.write(uIntTo4Bytes(UnsignedInt(1))) + outputStream.write(0x00) return 0 } @@ -69,9 +69,9 @@ object StringDatabaseKDBUtils { val initial = str.toByteArray(defaultCharset) val length = initial.size + 1 - os.write(uIntTo4Bytes(UnsignedInt(length))) - os.write(initial) - os.write(0x00) + outputStream.write(uIntTo4Bytes(UnsignedInt(length))) + outputStream.write(initial) + outputStream.write(0x00) return length } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt index e54399b2a..e7114ef75 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -35,6 +35,7 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryHistoryAdapter import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.security.ProtectedString @@ -321,6 +322,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context, * ------------- */ + fun setAttachmentCipherKey(cipherKey: Database.LoadedKey?) { + attachmentsAdapter.binaryCipherKey = cipherKey + } + private fun showAttachments(show: Boolean) { attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt b/app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt new file mode 100644 index 000000000..56ae257f0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/ExpirationView.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.text.util.Linkify +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.CompoundButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.widget.SwitchCompat +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.text.util.LinkifyCompat +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.UriUtil + +class ExpirationView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : ConstraintLayout(context, attrs, defStyle) { + + private var entryExpiresTextView: TextView + private var entryExpiresCheckBox: CompoundButton + + private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH + + private var fontInVisibility: Boolean = false + + var setOnDateClickListener: (() -> Unit)? = null + + init { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? + inflater?.inflate(R.layout.view_expiration, this) + + entryExpiresTextView = findViewById(R.id.expiration_text) + entryExpiresCheckBox = findViewById(R.id.expiration_checkbox) + + entryExpiresTextView.setOnClickListener { + if (entryExpiresCheckBox.isChecked) + setOnDateClickListener?.invoke() + } + entryExpiresCheckBox.setOnCheckedChangeListener { _, _ -> + assignExpiresDateText() + } + + fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(context) + } + + private fun assignExpiresDateText() { + entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) { + expiresInstant.getDateTimeString(resources) + } else { + resources.getString(R.string.never) + } + if (fontInVisibility) + entryExpiresTextView.applyFontVisibility() + } + + var expires: Boolean + get() { + return entryExpiresCheckBox.isChecked + } + set(value) { + if (!value) { + expiresInstant = DateInstant.IN_ONE_MONTH + } + entryExpiresCheckBox.isChecked = value + assignExpiresDateText() + } + + var expiryTime: DateInstant + get() { + return if (expires) + expiresInstant + else + DateInstant.NEVER_EXPIRE + } + set(value) { + if (expires) + expiresInstant = value + assignExpiresDateText() + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt index 2e25e9192..cc86a0296 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt @@ -31,7 +31,7 @@ import com.kunzisoft.keepass.R class ToolbarAction @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyle: Int = androidx.appcompat.R.attr.toolbarStyle) + defStyle: Int = R.attr.actionToolbarAppearance) : Toolbar(context, attrs, defStyle) { private var mActionModeCallback: ActionMode.Callback? = null @@ -39,7 +39,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context, private var isOpen = false init { - visibility = View.GONE + setNavigationIcon(R.drawable.ic_close_white_24dp) } fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode { @@ -55,8 +55,6 @@ class ToolbarAction @JvmOverloads constructor(context: Context, actionMode.finish() } - setNavigationIcon(R.drawable.ic_close_white_24dp) - open() return actionMode diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 720b6cfd7..df734a796 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.view import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator +import android.content.Context import android.graphics.Color import android.graphics.Paint import android.graphics.Typeface @@ -35,6 +36,7 @@ import android.text.style.ClickableSpan import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import android.widget.TextView +import android.widget.Toast import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.updatePadding import com.google.android.material.snackbar.Snackbar @@ -166,7 +168,17 @@ fun View.updateLockPaddingLeft() { )) } -fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) { +fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) { + if (!result.isSuccess) { + result.exception?.errorId?.let { errorId -> + Toast.makeText(this, errorId, Toast.LENGTH_LONG).show() + } ?: result.message?.let { message -> + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + } +} + +fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) { if (!result.isSuccess) { result.exception?.errorId?.let { errorId -> Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show() diff --git a/app/src/main/res/drawable/keepassdx_logo.png b/app/src/main/res/drawable/keepassdx_logo.png new file mode 100644 index 000000000..591f7e050 Binary files /dev/null and b/app/src/main/res/drawable/keepassdx_logo.png differ diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index 02d72bc52..93f4eda35 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -67,7 +67,7 @@ android:layout_height="?attr/actionBarSize" android:background="@color/transparent" android:theme="?attr/toolbarAppearance" - android:popupTheme="?attr/toolbarPopupAppearance" + app:popupTheme="?attr/toolbarPopupAppearance" app:layout_collapseMode="pin" tools:targetApi="lollipop"> diff --git a/app/src/main/res/layout/activity_entry_edit.xml b/app/src/main/res/layout/activity_entry_edit.xml index e04e5ac54..c24debc93 100644 --- a/app/src/main/res/layout/activity_entry_edit.xml +++ b/app/src/main/res/layout/activity_entry_edit.xml @@ -33,27 +33,14 @@ android:layout_height="wrap_content" android:fitsSystemWindows="true"> - - - - - + android:layout_height="?attr/actionBarSize" + android:theme="?attr/specialToolbarAppearance" + app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title" + app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle" + app:layout_constraintTop_toTopOf="parent" /> @@ -79,24 +66,23 @@ - + app:popupTheme="?attr/toolbarPopupAppearance" /> diff --git a/app/src/main/res/layout/activity_group.xml b/app/src/main/res/layout/activity_group.xml index 8c56fdb1b..8073ea918 100644 --- a/app/src/main/res/layout/activity_group.xml +++ b/app/src/main/res/layout/activity_group.xml @@ -64,7 +64,7 @@ android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="?attr/toolbarAppearance" - android:popupTheme="?attr/toolbarPopupAppearance" + app:popupTheme="?attr/toolbarPopupAppearance" android:elevation="4dp" tools:targetApi="lollipop"> + app:layout_constraintBottom_toBottomOf="parent" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_password.xml index 606225498..ddb806596 100644 --- a/app/src/main/res/layout/activity_password.xml +++ b/app/src/main/res/layout/activity_password.xml @@ -80,7 +80,7 @@ android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="?attr/toolbarAppearance" - android:popupTheme="?attr/toolbarPopupAppearance" + app:popupTheme="?attr/toolbarPopupAppearance" app:layout_collapseMode="pin" tools:targetApi="lollipop"> - - + - - - - - + android:layout_height="wrap_content" /> . --> - - - + - + + + + + + + + - - - + android:layout_marginStart="4dp" /> + + diff --git a/app/src/main/res/layout/item_attachment.xml b/app/src/main/res/layout/item_attachment.xml index 056f57b6e..95d69f18b 100644 --- a/app/src/main/res/layout/item_attachment.xml +++ b/app/src/main/res/layout/item_attachment.xml @@ -17,111 +17,138 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - - - + + - + android:layout_alignBottom="@+id/item_attachment_thumbnail" + android:background="?attr/cardBackgroundTransparentColor"> + + + - - - - - + + - + + + + + + + + + - + + + android:focusable="false"> + + + + + - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_default.xml b/app/src/main/res/layout/toolbar_default.xml index 87d0a97f2..7f83d9a4a 100644 --- a/app/src/main/res/layout/toolbar_default.xml +++ b/app/src/main/res/layout/toolbar_default.xml @@ -20,12 +20,13 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/view_expiration.xml b/app/src/main/res/layout/view_expiration.xml new file mode 100644 index 000000000..18fd548d2 --- /dev/null +++ b/app/src/main/res/layout/view_expiration.xml @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4975e880a..8ef8d10d9 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -553,4 +553,9 @@ Databázi nově načíst Přepsat externí změny uložením databáze nebo databázi včetně posledních změn nově načíst. Informace obsažená ve Vašem databázovém souboru by změněna mimo aplikaci. + GiB + MiB + KiB + B + Nemohu rozpoznat existující typ OTP v této formě, jeho validace patrně nebude generovat správný token. \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index fd44ec980..3c010d470 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -450,7 +450,8 @@ Klassisch Dunkel Himmel und Meer Roter Vulkan - Purple Pro + Purple Light + Purple Dark Datei Schreibrechte gewähren, um Datenbankänderungen zu speichern Einrichten einer Einmal-Passwortverwaltung (HOTP / TOTP), um ein Token zu generieren, das für die Zwei-Faktor-Authentifizierung (2FA) angefordert wird. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 7dee954af..af2c5bfc8 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -38,7 +38,7 @@ Διάρκεια αποθήκευσης στο πρόχειρο Επιλέξτε για αντιγραφή %1$s στο πρόχειρο Ανάκτηση κλειδιού βάσης δεδομένων… - Βάση Δεδομένων + Βάση δεδομένων Αποκρυπτογράφηση περιεχομένου βάσης δεδομένων … Χρήση ως προεπιλεγμένης βάσης δεδομένων Ψηφία @@ -46,7 +46,7 @@ \nΠαρέχεται ως έχει, με άδεια <strong>GPLv3</strong>, χωρίς καμία εγγύηση. Ανοίξτε την υπάρχουσα βάση δεδομένων Πρόσβαση - Ακύρωση + Άκυρο Σημειώσεις Επιβεβαίωση κωδικού Δημιουργήθηκε @@ -54,11 +54,11 @@ Αρχείο-Κλειδί Τροποποιήθηκε Δεν ήταν δυνατή η εύρεση δεδομένων εισόδου. - Κωδικός Πρόσβασης + Κωδικός πρόσβασης Αποθήκευση Τίτλος Διεύθυνση URL - Όνομα Χρήστη + Όνομα χρήστη Η ροή κρυπτογράφησης Arcfour δεν υποστηρίζεται. Το KeePassDX δε μπορεί να χειριστεί αυτή τη διεύθυνση URI. Δεν ήταν δυνατή η δημιουργία αρχείου @@ -82,7 +82,7 @@ Αρχείο κλειδί Μήκος Κωδικός - Κωδικός Πρόσβασης + Κωδικός πρόσβασης Δεν ήταν δυνατή η ανάγνωση διαπιστευτηρίων. Λάθος αλγόριθμος. Δεν ήταν δυνατή η αναγνώριση της μορφής της βάσης δεδομένων. @@ -258,7 +258,7 @@ Ευχαριστούμε πολύ για τη συνεισφορά σας. Εργαζόμαστε σκληρά για να διαθέσουμε αυτό το χαρακτηριστικό γρήγορα. Θυμηθείτε να ενημερώνετε την εφαρμογή σας, εγκαθιστώντας νέες εκδόσεις. - Download + Λήψη Συνεισφορά ChaCha20 AES @@ -270,7 +270,7 @@ Αντιγραφή Μετακίνηση Επικόλληση - Ακύρωση + Άκυρο Εάν αποτύχει η αυτόματη διαγραφή του προχείρου, διαγράψτε το ιστορικό του χειροκίνητα. Προειδοποίηση: Το πρόχειρο μοιράζεται από όλες τις εφαρμογές. Αν αντιγράφονται ευαίσθητα δεδομένα, άλλο λογισμικό μπορεί να το ανακτήσει. Να μην επιτρέπεται κανένα κύριο κλειδί @@ -552,4 +552,10 @@ Αντικαταστήστε τις εξωτερικές τροποποιήσεις αποθηκεύοντας τη βάση δεδομένων ή φορτώστε την ξανά με τις πιο πρόσφατες αλλαγές. Οι πληροφορίες που περιέχονται στο αρχείο της βάσης δεδομένων σας έχουν τροποποιηθεί εκτός της εφαρμογής. Επαναφόρτωση βάσης δεδομένων + GiB + MiB + KiB + B + Ο υπάρχων τύπος OTP δεν αναγνωρίζεται από αυτήν τη φόρμα, η επικύρωσή του ενδέχεται να μην δημιουργεί πλέον σωστά το token. + Ακυρώθηκε! \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml new file mode 100644 index 000000000..b470d8700 --- /dev/null +++ b/app/src/main/res/values-eo/strings.xml @@ -0,0 +1,150 @@ + + + Aspekto + Historio + Versio %1$s + Averto + Majuskloj + Serĉrezultoj + Serĉi + Uzantnomo + Titolo + Naturala ordo + Ordigi + Serĉi + Ne ĉesigu la aplikaĵon… + Datumbaza konservado… + Paralelado + Memoruzado + Elektreĝimo + Konservreĝimo + Serĉreĝimo + Kontraŭskribe protektita + Protekto + Laborado… + Nova datumbaza kreado… + Subdomajna serĉo + Rapida serĉo + Krei novan datumbazon + Malfermi ekzistantan datumbazon + Instalu TTT-legilon por malfermi ĉi tiun retadreson. + Nieniu serĉrezulto + Neniam + Minuso + Forigi historion + Historia restaŭro + Malplenigi la rubujon + Modifebla + Kontraŭskribe protektita + Iri al retadreso + Montri pasvorton + Serĉi + Malfermi + Reŝargi datumbazon + Konservi datumbazon + Ŝlosi datumbazon + Kaŝi pasvorton + Nuligi + Forigi + Algui + Movi + Kopii + Redakti + Sekureca agordoj + Datumbaza agordoj + Minusklo + Teksta grandeco en la elementa listo + Aplikaĵaj agordoj + Agordoj + Kopio de %1$s + Pri + Kaŝi pasvortojn sub (***) defaŭlte + Kaŝi pasvortojn + Datumbaza ŝarĝado… + Datumbaza kreado… + Vidigi uzantnomojn en elementaj listoj + Vidigi uzantnomojn + Longeco + La ŝlosila dosiero estas malplena. + Malĝusta algoritmo. + Pasvorto + Pasvorto + Longeco + Ŝlosila dosiero + Grupa nomo + Generita pasvorto + Konfirmu pasvorton + Generi pasvorton + Dosiermastrumilo + Kampa valoro + Kampa nomo + La kampa nomo jam ekzistas. + Ne povis konservi datumbazon. + Vi ne povas kopii grupon ĉi tie. + Vi ne povas kopii elementon ĉi tie. + Vi ne povas movigi elementon ĉi tie. + Ĉi tiu etikedo jam ekzistas. + La pasvortoj ne similas. + Ne povas ŝarĝi vian datumbazon. + Elektu ŝlosilan dosieron. + Tajpu nomon. + Certigu ke la vojo estas ĝusta. + Ne povas legi datumbazon. + Ne povas krei dosieron + Uzantnomo + Retadreso + Titolo + Konservi + Pasvorto + Ne povas trovi ian elementan datumon. + Modifita + Ŝlosila dosiero + Aldonaĵoj + Historio + Senvalidiĝos + Kreita + Konfirmu pasvorton + Notoj + Rezigni + Ciferoj + Uzi kiel defaŭlta datumbazo + Datumbazo + Elekti por kopii %1$s al tondejo + Fermi kampojn + Forigi + Forigi kampon + Aldoni kampon + Pasvorta longeco + Pasvorta generilo + Forĵeti + Ĉu forĵeti ŝanĝojn\? + Validigi + Elementa piktogramo + Identigila informo + Dosiera informo + Aldoni eron + Aldoni grupon + Aldoni elementon + Aldoni nodon + Malfermi dosieron + Fono + Tondeja tempolimo + Ne povis purigi tondejon + Tondeja eraro + Tondejo purigita + Permesi + Krampoj + Aplikaĵo + Tempolimo de aplikaĵo + Ĉifrada algoritmo + Ĉifrado + Sekureco + Mastra ŝlosilo + Aldoni grupon + Redakti elementon + Aldoni elementon + Akcepti + Hejmpaĝo + Kontribuo + Kontakto + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2bfed9ce2..08fa29360 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -547,4 +547,16 @@ Mostrar UUID No es posible reconstruir adecuadamente la lista. La URI de la base de datos no puede ser recuperada. + Intenta mostrar sugerencias de autocompletado directamente desde un teclado compatible + Añadidas sugerencias de autocompletado. + Acceso al archivo revocado por el administrador de archivos, cierra la base de datos y vuelva a abrirla desde su ubicación. + GiB + MiB + KiB + B + Sugerencias en línea + Sobrescribir las modificaciones externas guardando la base de datos o recargándola con los últimos cambios. + La información contenida en su archivo de base de datos ha sido modificada fuera de la aplicación. + Recargar la base de datos + El tipo de OTP existente no es reconocido por este formulario, su validación ya no puede generar correctamente el token. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0275a145c..788f82b7b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -281,13 +281,14 @@ Thème de l’application Thème utilisé dans l’application - Thème Jour - Thème Nuit - Thème Noir - Thème Foncé Classique - Thème Ciel et Océan - Thème Rouge Volcan - Thème Pro Violet + Jour + Nuit + Noir + Foncé Classique + Ciel et Océan + Rouge Volcan + Pourpre Lumineux + Pourpre Obscur Collection d’icônes Collection d’icônes utilisées dans l’application @@ -561,4 +562,10 @@ Accès au dossier révoqué par le gestionnaire de fichiers, fermer la base de données et la rouvrir à partir de son emplacement. Les informations contenues dans votre fichier de base de données ont été modifiées en dehors de l\'application. Recharger la base de données + Gibioctets + Mébioctets + Kibioctets + Octets + Le type OTP existant n\'est pas reconnu par ce formulaire, sa validation peut ne plus générer correctement le jeton. + Annulé ! \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ada9f8ff9..9a13a8aed 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -75,13 +75,13 @@ Spremi Naslov Postavi jednokratnu lozinku - Tip OTP-a + Vrsta jednokratne lozinke Tajna Razdoblje (u sekundama) Brojač Znamenke Algoritam - OTP + Jednokratna lozinka URL Korisničko ime Odaberi datoteku ključa. @@ -234,7 +234,7 @@ Nije moguće stvoriti datoteku Nije moguće čitati bazu podataka. Provjeri putanju do datoteke. - Neispravan OTP tajni ključ. + Neispravna tajna jednokratne lozinke. Upiši ime. Nema dovoljno memorije za učitavanje cijele baze podataka. Nije moguće učitati bazu podataka. @@ -416,7 +416,7 @@ Paket ikona Doprinesi Zahvaljujemo na doprinosu. - Postavi OTP + Postavi jednokratnu lozinku Dodaj element Kupnjom <strong>pro</strong> verzije, Prikaži polja unosa u Magikeyboardu prilikom prikaza unosa @@ -536,4 +536,9 @@ Prepiši vanjske promjene spremanjem baze podataka ili je ponovo učitaj s najnovijim promjenama. Podaci u datoteci tvoje baze podataka izmijenjeni su izvan programa. Ponovo učitaj bazu podataka + Ovaj obrazac ne prepoznaje postojeću vrstu jednokratne lozinke. Provjera valjanosti možda više neće pravilno generirati token. + GiB + MiB + KiB + B \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9f5b5db4d..2eaa5d595 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -362,7 +362,7 @@ Sblocco avanzato Cronologia Imposta password usa e getta - Tipo di password usa e getta + Tipo di password OTP Segreto Periodo (secondi) Contatore @@ -555,4 +555,5 @@ Sovrascrivi le modifiche esterne salvano il database o ricaricalo con gli ultimi cambiamenti. I dati nel tuo database sono stati modificati al di fuori di questa app. Ricarica database + Il tipo di OTP esistente non è riconosciuto da questo modulo, la sua convalida potrebbe non generare più correttamente il token. \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 035412598..61bb0db6b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -499,7 +499,8 @@ Classic Dark Sky and Ocean Red Volcano - Purple Pro + Purple Light + Purple Dark アイコンパック アプリで使用するアイコンパック diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index d9a4a2507..b34a5b7c0 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -425,4 +425,20 @@ Søkemodus Feltnavnet finnes allerede. Legg til element + Avansert databaseopplåsing + Argon2id + Argon2d + B + Viser UUID-en tilhørende en oppføring + Vis UUID + GiB + MiB + KiB + Last opp + Legg til vedlegg + Lagre søkeinfo + Lukk database + Velg oppføring + Tilbake til forrige tastatur + Egendefinerte felter \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 309e9c809..2eb29a0a8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -552,4 +552,9 @@ Nadpisz zewnętrzne modyfikacje, zapisując bazę danych lub przeładuj ją z najnowszymi zmianami. Informacje zawarte w pliku bazy danych zostały zmodyfikowane poza aplikacją. Załaduj ponownie bazę danych + GiB + MiB + KiB + B + Istniejący typ OTP nie jest rozpoznawany przez ten formularz, jego walidacja może już nie generować poprawnie tokenu. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c9b312fc9..95df8a771 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -552,4 +552,10 @@ Сохранить базу, перезаписав внешние изменения, или перезагрузить её с последними изменениями. Информация, содержащаяся в файле базы, была изменена вне этого приложения. Перезагрузить базу + Существующий тип OTP не распознаётся данной формой, его проверка больше не может корректно генерировать токен. + ГиБ + МиБ + КиБ + Б + Отменено! \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f72af2d2a..51f38b2de 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -406,7 +406,7 @@ İndir %1$s Başlatılıyor… Devam ediyor: %1$d%% - Sonlandırılıyor… + Sonuçlandırılıyor… Tamamlandı! Süresi dolmuş girdileri gizle Süresi dolmuş girdiler gösterilmez @@ -536,4 +536,10 @@ Veri tabanını kaydederek veya en son değişikliklerle yeniden yükleyerek harici değişikliklerin üzerine yazın. Veri tabanı dosyanızda bulunan bilgiler, uygulamanın dışında değiştirildi. Veri tabanını yeniden yükle + GiB + MiB + KiB + B + Mevcut OTP türü bu form tarafından tanınmıyor, doğrulaması artık belirteci doğru şekilde oluşturmayabilir. + İptal edildi! \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e8c80deca..8bcbe1e43 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -552,4 +552,10 @@ Перезаписати зовнішні зміни, зберігши базу даних або перезавантажте її з найновішими змінами. Відомості, що містяться у файлі бази даних, змінено за межами застосунку. Перезавантажити базу даних + ГіБ + МіБ + КіБ + Б + Ця форма не розпізнає наявний тип OTP, його перевірка може надалі створювати не дійсний токен. + Скасовано! \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f6d58bb79..cc6ecff56 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -552,4 +552,10 @@ 通过保存数据库或用最新的更改重新加载数据库来覆盖外部修改。 数据库文件中包含的信息已在应用程序之外被修改。 重新加载数据库 + GiB + 兆字节 + 千位字节 + 字节 + 现有的 OTP 类型未被此表单所识别,其验证可能不再正确生成令牌。 + 已取消! \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 8414e6d24..73f6b73cf 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -19,15 +19,14 @@ --> + - - + - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8860d6fdf..f313fa6ef 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -20,11 +20,15 @@ #00ffffff #ffffff + #E0FFFFFF #000000 #0A0A0A + #101010 #0e0e0e + #E00E0E0E #bdbdbd #9e9e9e + #E0424242 #ffa726 #fb8c00 @@ -55,12 +59,17 @@ #9048bc #743998 #653383 + #4E1C6B + #451D5D #FAFAFA #303030 #000000 #F2EDFA #F7F3FD + #E0F7F3FD + #361748 + #E0361748 #555555 #000000 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 299a25928..a0713ede3 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -332,6 +332,7 @@ KeepassDXStyle_Blue KeepassDXStyle_Red KeepassDXStyle_Purple + KeepassDXStyle_Purple_Dark @string/list_style_name_light @string/list_style_name_night @@ -340,6 +341,7 @@ @string/list_style_name_blue @string/list_style_name_red @string/list_style_name_purple + @string/list_style_name_purple_dark @string/material_resource_id diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3aaa72ef0..d1d3f2944 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -514,6 +514,7 @@ In progress: %1$d%% Finalizing… Complete! + Canceled! Rijndael (AES) Twofish ChaCha20 @@ -563,7 +564,8 @@ Classic Dark Sky and Ocean Red Volcano - Purple Pro + Purple Light + Purple Dark Icon pack Icon pack used in the app diff --git a/app/src/main/res/values/style_black.xml b/app/src/main/res/values/style_black.xml index af1813020..3b20887df 100644 --- a/app/src/main/res/values/style_black.xml +++ b/app/src/main/res/values/style_black.xml @@ -30,12 +30,14 @@ @color/green_light @color/background_dark @style/KeepassDXStyle.Toolbar.Black + @style/KeepassDXStyle.Toolbar.Popup.Black @style/KeepassDXStyle.Toolbar.Home.Black @style/KeepassDXStyle.Toolbar.Special.Black @style/KeepassDXStyle.Black.Dialog @style/KeepassDXStyle.Black.Dialog @style/KeepassDXStyle.ActionMode.Black @style/KeepassDXStyle.Cardview.Black + @color/dark_transparent + + @@ -62,4 +64,9 @@ @color/blue_dark @color/blue_dark + + \ No newline at end of file diff --git a/app/src/main/res/values/style_dark.xml b/app/src/main/res/values/style_dark.xml index 55c1b1f2c..a5c8a261e 100644 --- a/app/src/main/res/values/style_dark.xml +++ b/app/src/main/res/values/style_dark.xml @@ -35,6 +35,7 @@ @style/KeepassDXStyle.Dark.Dialog @style/KeepassDXStyle.ActionMode.Dark @style/KeepassDXStyle.Cardview.Dark + @color/dark_transparent + + diff --git a/app/src/main/res/values/style_purple_dark.xml b/app/src/main/res/values/style_purple_dark.xml new file mode 100644 index 000000000..abaed3b33 --- /dev/null +++ b/app/src/main/res/values/style_purple_dark.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/style_red.xml b/app/src/main/res/values/style_red.xml index 200bcf651..778540c4a 100644 --- a/app/src/main/res/values/style_red.xml +++ b/app/src/main/res/values/style_red.xml @@ -24,13 +24,15 @@ @color/red_dark @color/orange @color/orange_light - @color/red + @color/orange @color/red @color/red_lighter @color/background_night @style/KeepassDXStyle.Toolbar.Red @style/KeepassDXStyle.Toolbar.Home.Red @style/KeepassDXStyle.Toolbar.Special.Red + @style/KeepassDXStyle.Red.Dialog + @style/KeepassDXStyle.Red.Dialog @style/KeepassDXStyle.ActionMode.Red @@ -57,4 +59,9 @@ @color/orange @color/orange + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ca871ad06..7aec9fbd8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -55,6 +55,7 @@ @style/KeepassDXStyle.TextAppearance @color/background_light + #DDFFFFFF @color/text_color_light @color/colorText @@ -74,7 +75,7 @@ @style/KeepassDXStyle.Toolbar.Light - @style/KeepassDXStyle.Light.Toolbar.Popup + @style/KeepassDXStyle.Toolbar.Popup.Light @style/KeepassDXStyle.Toolbar.Home.Light @style/KeepassDXStyle.Toolbar.Special.Light @style/KeepassDXStyle.Toolbar.Action @@ -112,6 +113,7 @@ @style/KeepassDXStyle.TextAppearance @color/background_night + @color/grey_dark_transparent @color/text_color_night @color/colorTextInverse @@ -131,7 +133,7 @@ @style/KeepassDXStyle.Toolbar.Night - @style/KeepassDXStyle.Night.Toolbar.Popup + @style/KeepassDXStyle.Toolbar.Popup.Night @style/KeepassDXStyle.Toolbar.Home.Night @style/KeepassDXStyle.Toolbar.Special.Night @style/KeepassDXStyle.Toolbar.Action @@ -146,17 +148,11 @@ + +