diff --git a/CHANGELOG b/CHANGELOG index 854659cef..9543ff5e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +KeePassDX(2.9.14) + * Add custom icons #96 + * Dark Themes #532 #714 + * Fix binary deduplication #715 + * Fix IconId #901 + * Resize image stream dynamically to prevent slowdown #919 + * Small changes #795 #900 #903 #909 #914 + KeePassDX(2.9.13) * Binary image viewer #473 #749 * Fix TOTP plugin settings #878 diff --git a/app/build.gradle b/app/build.gradle index 34df6faf8..2dd268461 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,19 +1,18 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion 30 - buildToolsVersion '30.0.3' - ndkVersion '21.3.6528147' + buildToolsVersion "30.0.3" + ndkVersion "21.4.7075529" defaultConfig { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 30 - versionCode = 57 - versionName = "2.9.13" + versionCode = 65 + versionName = "2.9.14" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" @@ -51,7 +50,11 @@ android { buildConfigField "String", "BUILD_VERSION", "\"libre\"" buildConfigField "boolean", "FULL_VERSION", "true" buildConfigField "boolean", "CLOSED_STORE", "false" - buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\",\"KeepassDXStyle_Purple_Dark\"}" + buildConfigField "String[]", "STYLES_DISABLED", + "{\"KeepassDXStyle_Red\"," + + "\"KeepassDXStyle_Red_Night\"," + + "\"KeepassDXStyle_Purple\"," + + "\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" } pro { @@ -70,7 +73,13 @@ 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\",\"KeepassDXStyle_Purple_Dark\"}" + buildConfigField "String[]", "STYLES_DISABLED", + "{\"KeepassDXStyle_Blue\"," + + "\"KeepassDXStyle_Blue_Night\"," + + "\"KeepassDXStyle_Red\"," + + "\"KeepassDXStyle_Red_Night\"," + + "\"KeepassDXStyle_Purple\"," + + "\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ] } @@ -104,18 +113,19 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.biometric:biometric:1.1.0-rc01' // Lifecycle - LiveData - ViewModel - Coroutines implementation "androidx.core:core-ktx:1.3.2" implementation 'androidx.fragment:fragment-ktx:1.2.5' - // WARNING: To upgrade with style, bug in edit text - implementation 'com.google.android.material:material:1.0.0' + // WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923 + implementation 'com.google.android.material:material:1.1.0' // Database implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" // Autofill - implementation "androidx.autofill:autofill:1.1.0-rc01" + implementation "androidx.autofill:autofill:1.1.0" // Crypto implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' // Time @@ -125,7 +135,6 @@ dependencies { // Education implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' // Apache Commons - implementation 'commons-collections:commons-collections:3.2.2' implementation 'commons-io:commons-io:2.8.0' implementation 'commons-codec:commons-codec:1.15' // Icon pack diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryDataTest.kt similarity index 58% rename from app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt rename to app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryDataTest.kt index b7bf2d8b4..2ce99e88d 100644 --- a/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryAttachmentTest.kt +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/stream/BinaryDataTest.kt @@ -3,7 +3,8 @@ 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.database.element.database.BinaryByte +import com.kunzisoft.keepass.database.element.database.BinaryFile import com.kunzisoft.keepass.stream.readAllBytes import com.kunzisoft.keepass.utils.UriUtil import junit.framework.TestCase.assertEquals @@ -11,10 +12,9 @@ import org.junit.Test import java.io.DataInputStream import java.io.File import java.io.InputStream -import java.lang.Exception -import java.security.MessageDigest +import kotlin.random.Random -class BinaryAttachmentTest { +class BinaryDataTest { private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context @@ -27,9 +27,9 @@ class BinaryAttachmentTest { private val loadedKey = Database.LoadedKey.generateNewCipherKey() - private fun saveBinary(asset: String, binaryAttachment: BinaryAttachment) { + private fun saveBinary(asset: String, binaryData: BinaryFile) { context.assets.open(asset).use { assetInputStream -> - binaryAttachment.getOutputDataStream(loadedKey).use { binaryOutputStream -> + binaryData.getOutputDataStream(loadedKey).use { binaryOutputStream -> assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer -> binaryOutputStream.write(buffer) } @@ -39,62 +39,80 @@ class BinaryAttachmentTest { @Test fun testSaveTextInCache() { - val binaryA = BinaryAttachment(fileA) - val binaryB = BinaryAttachment(fileB) + val binaryA = BinaryFile(fileA) + val binaryB = BinaryFile(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()) + assertEquals("Save text binary length failed.", binaryA.getSize(), binaryB.getSize()) + assertEquals("Save text binary MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash()) } @Test fun testSaveImageInCache() { - val binaryA = BinaryAttachment(fileA) - val binaryB = BinaryAttachment(fileB) + val binaryA = BinaryFile(fileA) + val binaryB = BinaryFile(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()) + assertEquals("Save image binary length failed.", binaryA.getSize(), binaryB.getSize()) + assertEquals("Save image binary failed.", binaryA.binaryHash(), binaryB.binaryHash()) } @Test fun testCompressText() { - val binaryA = BinaryAttachment(fileA) - val binaryB = BinaryAttachment(fileB) - val binaryC = BinaryAttachment(fileC) + val binaryA = BinaryFile(fileA) + val binaryB = BinaryFile(fileB) + val binaryC = BinaryFile(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()) + assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize()) + assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash()) binaryB.decompress(loadedKey) - assertEquals("Decompress text length failed.", binaryB.length, binaryC.length) - assertEquals("Decompress text MD5 failed.", binaryB.md5(), binaryC.md5()) + assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize()) + assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash()) } @Test fun testCompressImage() { - val binaryA = BinaryAttachment(fileA) - var binaryB = BinaryAttachment(fileB) - val binaryC = BinaryAttachment(fileC) + val binaryA = BinaryFile(fileA) + var binaryB = BinaryFile(fileB) + val binaryC = BinaryFile(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) + assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize()) + assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash()) + binaryB = BinaryFile(fileB, true) binaryB.decompress(loadedKey) - assertEquals("Decompress image length failed.", binaryB.length, binaryC.length) - assertEquals("Decompress image failed.", binaryB.md5(), binaryC.md5()) + assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize()) + assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash()) + } + + @Test + fun testCompressBytes() { + val byteArray = ByteArray(50) + Random.nextBytes(byteArray) + val binaryA = BinaryByte(byteArray) + val binaryB = BinaryByte(byteArray) + val binaryC = BinaryByte(byteArray) + binaryA.compress(loadedKey) + binaryB.compress(loadedKey) + assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true) + assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize()) + assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash()) + binaryB.decompress(loadedKey) + assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false) + assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize()) + assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash()) } @Test fun testReadText() { - val binaryA = BinaryAttachment(fileA) + val binaryA = BinaryFile(fileA) saveBinary(TEST_TEXT_ASSET, binaryA) assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET), binaryA.getInputDataStream(loadedKey))) @@ -102,7 +120,7 @@ class BinaryAttachmentTest { @Test fun testReadImage() { - val binaryA = BinaryAttachment(fileA) + val binaryA = BinaryFile(fileA) saveBinary(TEST_IMAGE_ASSET, binaryA) assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET), binaryA.getInputDataStream(loadedKey))) @@ -132,20 +150,6 @@ class BinaryAttachmentTest { } } - 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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7bcdcb9a3..0f4d196b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -129,6 +129,9 @@ + 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 526736668..955a547d3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -47,7 +47,6 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.education.EntryActivityEducation -import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.StreamDirection @@ -242,7 +241,9 @@ class EntryActivity : LockingActivity() { val entryInfo = entry.getEntryInfo(mDatabase) // Assign title icon - titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor) + titleIconView?.let { iconView -> + mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor) + } // Assign title text val entryTitle = entryInfo.title 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 146dccd21..167b0581c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -43,6 +43,7 @@ import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE +import com.kunzisoft.keepass.activities.fragments.EntryEditFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.lock.LockingActivity @@ -78,7 +79,6 @@ import java.util.* import kotlin.collections.ArrayList class EntryEditActivity : LockingActivity(), - IconPickerDialogFragment.IconPickerListener, EntryCustomFieldDialogFragment.EntryCustomFieldListener, GeneratePasswordDialogFragment.GeneratePasswordListener, SetOTPDialogFragment.CreateOtpListener, @@ -172,10 +172,14 @@ class EntryEditActivity : LockingActivity(), val parentIcon = mParent?.icon tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true) // Set default icon - if (parentIcon != null - && parentIcon.iconId != IconImage.UNKNOWN_ID - && parentIcon.iconId != IconImageStandard.FOLDER) { - tempEntryInfo?.icon = parentIcon + if (parentIcon != null) { + if (parentIcon.custom.isUnknown + && parentIcon.standard.id != IconImageStandard.FOLDER_ID) { + tempEntryInfo?.icon = IconImage(parentIcon.standard) + } + if (!parentIcon.custom.isUnknown) { + tempEntryInfo?.icon = IconImage(parentIcon.custom) + } } // Set default username tempEntryInfo?.username = mDatabase?.defaultUsername ?: "" @@ -204,7 +208,7 @@ class EntryEditActivity : LockingActivity(), .replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG) .commit() entryEditFragment?.apply { - drawFactory = mDatabase?.drawFactory + drawFactory = mDatabase?.iconDrawableFactory setOnDateClickListener = { expiryTime.date.let { expiresDate -> val dateTime = DateTime(expiresDate) @@ -219,8 +223,8 @@ class EntryEditActivity : LockingActivity(), openPasswordGenerator() } // Add listener to the icon - setOnIconViewClickListener = View.OnClickListener { - IconPickerDialogFragment.launch(this@EntryEditActivity) + setOnIconViewClickListener = { iconImage -> + IconPickerActivity.launch(this@EntryEditActivity, iconImage) } setOnRemoveAttachment = { attachment -> mAttachmentFileBinderManager?.removeBinaryAttachment(attachment) @@ -481,7 +485,7 @@ class EntryEditActivity : LockingActivity(), private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) { val compression = mDatabase?.compressionForNewEntry() ?: false - mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment -> + mDatabase?.buildNewBinaryAttachment(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment -> val entryAttachment = Attachment(fileName, binaryAttachment) // Ask to replace the current attachment if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) || @@ -497,9 +501,12 @@ class EntryEditActivity : LockingActivity(), override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> + entryEditFragment?.icon = icon + } + mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri -> uri?.let { attachmentToUploadUri -> - // TODO Async to get the name UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile -> documentFile.name?.let { fileName -> if (documentFile.length() > MAX_WARNING_BINARY_FILE) { @@ -565,7 +572,7 @@ class EntryEditActivity : LockingActivity(), // Delete temp attachment if not used mTempAttachments.forEach { tempAttachmentState -> val tempAttachment = tempAttachmentState.attachment - mDatabase?.binaryPool?.let { binaryPool -> + mDatabase?.attachmentPool?.let { binaryPool -> if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { mDatabase?.removeAttachmentIfNotUsed(tempAttachment) } @@ -711,12 +718,6 @@ class EntryEditActivity : LockingActivity(), } } - override fun iconPicked(bundle: Bundle) { - IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon -> - entryEditFragment?.icon = icon - } - } - 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 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 e2c4672c5..d948f9246 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -63,14 +63,13 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel -import kotlinx.android.synthetic.main.activity_file_selection.* import java.io.FileNotFoundException class FileDatabaseSelectActivity : SpecialModeActivity(), AssignMasterKeyDialogFragment.AssignPasswordDialogListener { // Views - private var coordinatorLayout: CoordinatorLayout? = null + private lateinit var coordinatorLayout: CoordinatorLayout private var createDatabaseButtonView: View? = null private var openDatabaseButtonView: View? = null @@ -217,7 +216,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), resultError = "$resultError $resultMessage" } Log.e(TAG, resultError) - Snackbar.make(activity_file_selection_coordinator_layout, + Snackbar.make(coordinatorLayout, resultError, Snackbar.LENGTH_LONG).asError().show() } @@ -238,9 +237,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), private fun fileNoFoundAction(e: FileNotFoundException) { val error = getString(R.string.file_not_found_content) Log.e(TAG, error, e) - coordinatorLayout?.let { - Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show() - } + Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() } private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { @@ -344,7 +341,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } } catch (e: Exception) { val error = getString(R.string.error_create_database_file) - Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() + Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() Log.e(TAG, error, e) } } @@ -372,9 +369,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), .show(supportFragmentManager, "passwordDialog") } else { val error = getString(R.string.error_create_database) - coordinatorLayout?.let { - Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show() - } + Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() Log.e(TAG, error) } } 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 4a59aa394..d7b42797c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -45,6 +45,7 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* +import com.kunzisoft.keepass.activities.fragments.ListNodesFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode @@ -59,7 +60,6 @@ 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 @@ -81,7 +81,6 @@ import org.joda.time.DateTime class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListener, - IconPickerDialogFragment.IconPickerListener, DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener, ListNodesFragment.NodeClickListener, @@ -105,7 +104,6 @@ class GroupActivity : LockingActivity(), private var mDatabase: Database? = null private var mListNodesFragment: ListNodesFragment? = null - private var mCurrentGroupIsASearch: Boolean = false private var mRequestStartupSearch = true private var actionNodeMode: ActionMode? = null @@ -172,7 +170,7 @@ class GroupActivity : LockingActivity(), } mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState) - mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action + val currentGroupIsASearch = mCurrentGroup?.isVirtual == true Log.i(TAG, "Started creating tree") if (mCurrentGroup == null) { @@ -181,13 +179,13 @@ class GroupActivity : LockingActivity(), } var fragmentTag = LIST_NODES_FRAGMENT_TAG - if (mCurrentGroupIsASearch) + if (currentGroupIsASearch) fragmentTag = SEARCH_FRAGMENT_TAG // Initialize the fragment with the list mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment? if (mListNodesFragment == null) - mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch) + mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, currentGroupIsASearch) // Attach fragment to content view supportFragmentManager.beginTransaction().replace( @@ -206,9 +204,11 @@ class GroupActivity : LockingActivity(), // Add listeners to the add buttons addNodeButtonView?.setAddGroupClickListener { - GroupEditDialogFragment.build() - .show(supportFragmentManager, - GroupEditDialogFragment.TAG_CREATE_GROUP) + GroupEditDialogFragment.create(GroupInfo().apply { + if (mCurrentGroup?.allowAddNoteInGroup == true) { + notes = "" + } + }).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) } addNodeButtonView?.setAddEntryClickListener { mCurrentGroup?.let { currentGroup -> @@ -346,9 +346,7 @@ class GroupActivity : LockingActivity(), ACTION_DATABASE_RELOAD_TASK -> { // Reload the current activity if (result.isSuccess) { - startActivity(intent) - finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + reload() } else { this.showActionErrorIfNeeded(result) finish() @@ -367,6 +365,14 @@ class GroupActivity : LockingActivity(), Log.i(TAG, "Finished creating tree") } + private fun reload() { + // Reload the current activity + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + mDatabase?.wasReloaded = false + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) @@ -375,13 +381,10 @@ class GroupActivity : LockingActivity(), manageSearchInfoIntent(intentNotNull) Log.d(TAG, "setNewIntent: $intentNotNull") setIntent(intentNotNull) - mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) { + if (Intent.ACTION_SEARCH == intentNotNull.action) { // only one instance of search in backstack deletePreviousSearchGroup() openGroup(retrieveCurrentGroup(intentNotNull, null), true) - true - } else { - false } } } @@ -465,12 +468,11 @@ class GroupActivity : LockingActivity(), private fun refreshSearchGroup() { deletePreviousSearchGroup() - if (mCurrentGroupIsASearch) + if (mCurrentGroup?.isVirtual == true) openGroup(retrieveCurrentGroup(intent, null), true) } private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? { - // Force read only if the database is like that mReadOnly = mDatabase?.isReadOnly == true || mReadOnly @@ -518,24 +520,21 @@ class GroupActivity : LockingActivity(), } } } - if (mCurrentGroupIsASearch) { - searchTitleView?.visibility = View.VISIBLE - } else { - searchTitleView?.visibility = View.GONE - } - // Assign icon - if (mCurrentGroupIsASearch) { + if (mCurrentGroup?.isVirtual == true) { + searchTitleView?.visibility = View.VISIBLE if (toolbar != null) { toolbar?.navigationIcon = null } iconView?.visibility = View.GONE } else { + searchTitleView?.visibility = View.GONE // Assign the group icon depending of IconPack or custom icon iconView?.visibility = View.VISIBLE - mCurrentGroup?.let { - if (mDatabase?.drawFactory != null) - iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor) + mCurrentGroup?.let { currentGroup -> + iconView?.let { imageView -> + mDatabase?.iconDrawableFactory?.assignDatabaseIcon(imageView, currentGroup.icon, mIconColor) + } if (toolbar != null) { if (mCurrentGroup?.containsParent() == true) @@ -550,20 +549,25 @@ class GroupActivity : LockingActivity(), // Assign number of children refreshNumberOfChildren() - // Show button if allowed - addNodeButtonView?.apply { + // Hide button + initAddButton() + } + private fun initAddButton() { + addNodeButtonView?.apply { + closeButtonIfOpen() // To enable add button - val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch - var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch + val addGroupEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true + var addEntryEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true mCurrentGroup?.let { - if (!it.allowAddEntryIfIsRoot()) + if (!it.allowAddEntryIfIsRoot) addEntryEnabled = it != mRootGroup && addEntryEnabled } enableAddGroup(addGroupEnabled) enableAddEntry(addEntryEnabled) - - if (actionNodeMode == null) + if (mCurrentGroup?.isVirtual == true) + hideButton() + else if (actionNodeMode == null) showButton() } } @@ -783,7 +787,7 @@ class GroupActivity : LockingActivity(), when (node.type) { Type.GROUP -> { mOldGroupToUpdate = node as Group - GroupEditDialogFragment.build(mOldGroupToUpdate!!.getGroupInfo()) + GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo()) .show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) } @@ -893,6 +897,9 @@ class GroupActivity : LockingActivity(), override fun onResume() { super.onResume() + if (mDatabase?.wasReloaded == true) { + reload() + } // Show the lock button lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { View.VISIBLE @@ -1120,13 +1127,6 @@ class GroupActivity : LockingActivity(), // Do nothing here } - override// For icon in create tree dialog - fun iconPicked(bundle: Bundle) { - (supportFragmentManager - .findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment) - .iconPicked(bundle) - } - override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) { mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters) } @@ -1165,6 +1165,13 @@ class GroupActivity : LockingActivity(), override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + // To create tree dialog for icon + IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> + (supportFragmentManager + .findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment) + .setIcon(icon) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) } @@ -1189,7 +1196,6 @@ class GroupActivity : LockingActivity(), mCurrentGroup = mListNodesFragment?.mainGroup // Remove search in intent deletePreviousSearchGroup() - mCurrentGroupIsASearch = false if (Intent.ACTION_SEARCH == intent.action) { intent.action = Intent.ACTION_DEFAULT intent.removeExtra(SearchManager.QUERY) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt new file mode 100644 index 000000000..bd5ef9a21 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt @@ -0,0 +1,324 @@ +/* + * 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.activities + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.viewModels +import androidx.appcompat.widget.Toolbar +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.fragment.app.commit +import com.google.android.material.snackbar.Snackbar +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.fragments.IconPickerFragment +import com.kunzisoft.keepass.activities.helpers.SelectFileHelper +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.database.element.icon.IconImage +import com.kunzisoft.keepass.database.element.icon.IconImageCustom +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.BinaryDatabaseManager +import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.view.asError +import com.kunzisoft.keepass.view.updateLockPaddingLeft +import com.kunzisoft.keepass.viewmodels.IconPickerViewModel +import kotlinx.coroutines.* + + +class IconPickerActivity : LockingActivity() { + + private lateinit var toolbar: Toolbar + private lateinit var coordinatorLayout: CoordinatorLayout + private lateinit var uploadButton: View + private var lockView: View? = null + + private var mIconImage: IconImage = IconImage() + + private val mainScope = CoroutineScope(Dispatchers.Main) + + private val iconPickerViewModel: IconPickerViewModel by viewModels() + private var mCustomIconsSelectionMode = false + private var mIconsSelected: List = ArrayList() + + private var mDatabase: Database? = null + + private var mSelectFileHelper: SelectFileHelper? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_icon_picker) + + mDatabase = Database.getInstance() + + toolbar = findViewById(R.id.toolbar) + toolbar.title = " " + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + updateIconsSelectedViews() + + coordinatorLayout = findViewById(R.id.icon_picker_coordinator) + + uploadButton = findViewById(R.id.icon_picker_upload) + if (mDatabase?.allowCustomIcons == true) { + uploadButton.setOnClickListener { + mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it) + } + uploadButton.setOnLongClickListener { + mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it) + true + } + } else { + uploadButton.visibility = View.GONE + } + + lockView = findViewById(R.id.lock_button) + lockView?.setOnClickListener { + lockAndExit() + } + + intent?.getParcelableExtra(EXTRA_ICON)?.let { + mIconImage = it + } + + if (savedInstanceState == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + add(R.id.icon_picker_fragment, IconPickerFragment.getInstance( + // Default selection tab + if (mIconImage.custom.isUnknown) + IconPickerFragment.IconTab.STANDARD + else + IconPickerFragment.IconTab.CUSTOM + ), ICON_PICKER_FRAGMENT_TAG) + } + } else { + mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage + } + + // Focus view to reinitialize timeout + findViewById(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this) + + mSelectFileHelper = SelectFileHelper(this) + + iconPickerViewModel.standardIconPicked.observe(this) { iconStandard -> + mIconImage.standard = iconStandard + // Remove the custom icon if a standard one is selected + mIconImage.custom = IconImageCustom() + setResult() + finish() + } + iconPickerViewModel.customIconPicked.observe(this) { iconCustom -> + // Keep the standard icon if a custom one is selected + mIconImage.custom = iconCustom + setResult() + finish() + } + iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected -> + mIconsSelected = iconsSelected + updateIconsSelectedViews() + } + iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded -> + if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) { + Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show() + iconCustomAdded.errorConsumed = true + } + uploadButton.isEnabled = true + } + iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved -> + if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) { + Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show() + iconCustomRemoved.errorConsumed = true + } + uploadButton.isEnabled = true + } + } + + private fun updateIconsSelectedViews() { + if (mIconsSelected.isEmpty()) { + mCustomIconsSelectionMode = false + toolbar.title = " " + } else { + mCustomIconsSelectionMode = true + toolbar.title = mIconsSelected.size.toString() + } + invalidateOptionsMenu() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putParcelable(EXTRA_ICON, mIconImage) + } + + override fun onResume() { + super.onResume() + + // Show the lock button + lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { + View.VISIBLE + } else { + View.GONE + } + + // Padding if lock button visible + toolbar.updateLockPaddingLeft() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + super.onCreateOptionsMenu(menu) + + if (mCustomIconsSelectionMode) { + menuInflater.inflate(R.menu.icon, menu) + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + if (mCustomIconsSelectionMode) { + iconPickerViewModel.deselectAllCustomIcons() + } else { + onBackPressed() + } + } + R.id.menu_delete -> { + mIconsSelected.forEach { iconToRemove -> + removeCustomIcon(iconToRemove) + } + } + } + + return super.onOptionsItemSelected(item) + } + + private fun addCustomIcon(iconToUploadUri: Uri?) { + uploadButton.isEnabled = false + mainScope.launch { + withContext(Dispatchers.IO) { + // on Progress with thread + val asyncResult: Deferred = async { + val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file) + UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile -> + if (documentFile.length() > MAX_ICON_SIZE) { + iconCustomState.errorStringId = R.string.error_file_to_big + } else { + mDatabase?.buildNewCustomIcon(UriUtil.getBinaryDir(this@IconPickerActivity)) { customIcon, binary -> + if (customIcon != null) { + iconCustomState.iconCustom = customIcon + BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(contentResolver, + iconToUploadUri, binary) + when { + binary == null -> { + } + binary.getSize() <= 0 -> { + } + mDatabase?.isCustomIconBinaryDuplicate(binary) == true -> { + iconCustomState.errorStringId = R.string.error_duplicate_file + } + else -> { + iconCustomState.error = false + } + } + if (iconCustomState.error) { + mDatabase?.removeCustomIcon(customIcon) + } + } + } + } + } + iconCustomState + } + withContext(Dispatchers.Main) { + asyncResult.await()?.let { customIcon -> + iconPickerViewModel.addCustomIcon(customIcon) + } + } + } + } + } + + private fun removeCustomIcon(iconImageCustom: IconImageCustom) { + uploadButton.isEnabled = false + iconPickerViewModel.deselectAllCustomIcons() + mDatabase?.removeCustomIcon(iconImageCustom) + iconPickerViewModel.removeCustomIcon( + IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file) + ) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri -> + addCustomIcon(uri) + } + } + + private fun setResult() { + setResult(Activity.RESULT_OK, Intent().apply { + putExtra(EXTRA_ICON, mIconImage) + }) + } + + override fun onBackPressed() { + setResult() + super.onBackPressed() + } + + companion object { + + private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG" + + private const val ICON_SELECTED_REQUEST = 15861 + private const val EXTRA_ICON = "EXTRA_ICON" + + private const val MAX_ICON_SIZE = 5242880 + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) { + if (requestCode == ICON_SELECTED_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage()) + } + } + } + + fun launch(context: Activity, + previousIcon: IconImage?) { + // Create an instance to return the picker icon + context.startActivityForResult( + Intent(context, + IconPickerActivity::class.java).apply { + if (previousIcon != null) + putExtra(EXTRA_ICON, previousIcon) + }, + ICON_SELECTED_REQUEST) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt index 5d12da108..143fd5763 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt @@ -26,6 +26,7 @@ import android.text.format.Formatter import android.util.Log import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.widget.ImageView import androidx.appcompat.widget.Toolbar import com.igreenwood.loupe.Loupe @@ -33,7 +34,8 @@ 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.* +import com.kunzisoft.keepass.tasks.BinaryDatabaseManager +import kotlin.math.max class ImageViewerActivity : LockingActivity() { @@ -47,17 +49,28 @@ class ImageViewerActivity : LockingActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) + val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container) val imageView: ImageView = findViewById(R.id.image_viewer_image) val progressView: View = findViewById(R.id.image_viewer_progress) + // Approximately, to not OOM and allow a zoom + val mImagePreviewMaxWidth = max( + resources.displayMetrics.widthPixels * 2, + resources.displayMetrics.heightPixels * 2 + ) + try { progressView.visibility = View.VISIBLE intent.getParcelableExtra(IMAGE_ATTACHMENT_TAG)?.let { attachment -> supportActionBar?.title = attachment.name - supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryAttachment.length) + supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryData.getSize()) - Attachment.loadBitmap(attachment, Database.getInstance().loadedCipherKey) { bitmapLoaded -> + BinaryDatabaseManager.loadBitmap( + attachment.binaryData, + Database.getInstance().loadedCipherKey, + mImagePreviewMaxWidth + ) { bitmapLoaded -> if (bitmapLoaded == null) { finish() } else { @@ -71,7 +84,7 @@ class ImageViewerActivity : LockingActivity() { finish() } - Loupe.create(imageView, image_viewer_container) { + Loupe.create(imageView, imageContainerView) { onViewTranslateListener = object : Loupe.OnViewTranslateListener { override fun onStart(view: ImageView) { 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 502a06581..df62f36b5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -36,6 +36,7 @@ import android.widget.* import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ActivityCompat import androidx.fragment.app.commit import com.google.android.material.snackbar.Snackbar @@ -71,7 +72,6 @@ import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel -import kotlinx.android.synthetic.main.activity_password.* import java.io.FileNotFoundException open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener { @@ -84,8 +84,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil private var confirmButtonView: Button? = null private var checkboxPasswordView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null - private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var infoContainerView: ViewGroup? = null + private lateinit var coordinatorLayout: CoordinatorLayout + private var advancedUnlockFragment: AdvancedUnlockFragment? = null private val databaseFileViewModel: DatabaseFileViewModel by viewModels() @@ -131,6 +132,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox) infoContainerView = findViewById(R.id.activity_password_info_container) + coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) @@ -271,7 +273,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil resultError = "$resultError $resultMessage" } Log.e(TAG, resultError) - Snackbar.make(activity_password_coordinator_layout, + Snackbar.make(coordinatorLayout, resultError, Snackbar.LENGTH_LONG).asError().show() } @@ -523,7 +525,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil || mSpecialMode == SpecialMode.REGISTRATION) ) { Log.e(TAG, getString(R.string.autofill_read_only_save)) - Snackbar.make(activity_password_coordinator_layout, + Snackbar.make(coordinatorLayout, R.string.autofill_read_only_save, Snackbar.LENGTH_LONG).asError().show() } else { 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 857176e73..b9a857688 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 @@ -120,8 +120,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { .setPositiveButton(android.R.string.ok) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> } - val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information) - credentialsInfo?.setOnClickListener { + rootView?.findViewById(R.id.credentials_information)?.setOnClickListener { UriUtil.gotoUrl(activity, R.string.credentials_explanation_url) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt index ebd3cc22e..d83b9155d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt @@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog import android.os.Bundle +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R @@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { activity?.let { activity -> // Use the Builder class for convenient dialog construction - val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply { + val builder = AlertDialog.Builder(activity).apply { val message = getString(R.string.contains_duplicate_uuid) + "\n\n" + getString(R.string.contains_duplicate_uuid_procedure) setMessage(message) 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 64082dd75..edf4917af 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 @@ -31,16 +31,17 @@ 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.IconPickerActivity 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.DateInstant -import com.kunzisoft.keepass.icons.assignDatabaseIcon +import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.view.ExpirationView import org.joda.time.DateTime -class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener { +class GroupEditDialogFragment : DialogFragment() { private var mDatabase: Database? = null @@ -112,8 +113,6 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP arguments?.apply { if (containsKey(KEY_ACTION_ID)) mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID)) - if (mEditGroupDialogAction === CREATION) - mGroupInfo.notes = "" if (containsKey(KEY_GROUP_INFO)) { mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo } @@ -144,7 +143,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP } iconButtonView.setOnClickListener { _ -> - IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment") + IconPickerActivity.launch(activity, mGroupInfo.icon) } return builder.create() @@ -204,13 +203,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP } private fun assignIconView() { - if (mDatabase?.drawFactory != null) { - iconButtonView.assignDatabaseIcon(mDatabase?.drawFactory!!, mGroupInfo.icon, iconColor) - } + mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor) } - override fun iconPicked(bundle: Bundle) { - mGroupInfo.icon = IconPickerDialogFragment.getIconStandardFromBundle(bundle) ?: mGroupInfo.icon + fun setIcon(icon: IconImage) { + mGroupInfo.icon = icon assignIconView() } @@ -242,15 +239,16 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP const val KEY_ACTION_ID = "KEY_ACTION_ID" const val KEY_GROUP_INFO = "KEY_GROUP_INFO" - fun build(): GroupEditDialogFragment { + fun create(groupInfo: GroupInfo): GroupEditDialogFragment { val bundle = Bundle() bundle.putInt(KEY_ACTION_ID, CREATION.ordinal) + bundle.putParcelable(KEY_GROUP_INFO, groupInfo) val fragment = GroupEditDialogFragment() fragment.arguments = bundle return fragment } - fun build(groupInfo: GroupInfo): GroupEditDialogFragment { + fun update(groupInfo: GroupInfo): GroupEditDialogFragment { val bundle = Bundle() bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal) bundle.putParcelable(KEY_GROUP_INFO, groupInfo) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt deleted file mode 100644 index 715b52451..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.activities.dialogs - -import android.app.Dialog -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.GridView -import android.widget.ImageView -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.widget.ImageViewCompat -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.FragmentActivity -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.icon.IconImageStandard -import com.kunzisoft.keepass.icons.IconPack -import com.kunzisoft.keepass.icons.IconPackChooser - - -class IconPickerDialogFragment : DialogFragment() { - - private var iconPickerListener: IconPickerListener? = null - private var iconPack: IconPack? = null - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - iconPickerListener = context as IconPickerListener - } catch (e: ClassCastException) { - // The activity doesn't implement the interface, throw exception - throw ClassCastException(context.toString() - + " must implement " + IconPickerListener::class.java.name) - } - } - - override fun onDetach() { - iconPickerListener = null - super.onDetach() - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - activity?.let { activity -> - val builder = AlertDialog.Builder(activity) - - iconPack = IconPackChooser.getSelectedIconPack(requireContext()) - - // Inflate and set the layout for the dialog - // Pass null as the parent view because its going in the dialog layout - val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null) - builder.setView(root) - - val currIconGridView = root.findViewById(R.id.IconGridView) - currIconGridView.adapter = ImageAdapter(activity) - - currIconGridView.setOnItemClickListener { _, _, position, _ -> - val bundle = Bundle() - bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position)) - iconPickerListener?.iconPicked(bundle) - dismiss() - } - - builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() } - - return builder.create() - } - return super.onCreateDialog(savedInstanceState) - } - - inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() { - - override fun getCount(): Int { - return iconPack?.numberOfIcons() ?: 0 - } - - override fun getItem(position: Int): Any? { - return null - } - - override fun getItemId(position: Int): Long { - return 0 - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val currentView: View = convertView - ?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater) - .inflate(R.layout.item_icon, parent, false) - - iconPack?.let { iconPack -> - val iconImageView = currentView.findViewById(R.id.icon_image) - iconImageView.setImageResource(iconPack.iconToResId(position)) - - // Assign color if icons are tintable - if (iconPack.tintable()) { - // Retrieve the textColor to tint the icon - val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) - ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK))) - ta.recycle() - } - } - - return currentView - } - } - - interface IconPickerListener { - fun iconPicked(bundle: Bundle) - } - - companion object { - - private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD" - - fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? { - return bundle.getParcelable(KEY_ICON_STANDARD) - } - - fun launch(activity: FragmentActivity) { - // Create an instance of the dialog fragment and show it - val dialog = IconPickerDialogFragment() - dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment") - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt index 487cd52e1..eb31824e8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt @@ -46,6 +46,7 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD import com.kunzisoft.keepass.otp.OtpTokenType import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.TokenCalculator +import com.kunzisoft.keepass.utils.UriUtil import java.util.* class SetOTPDialogFragment : DialogFragment() { @@ -223,13 +224,16 @@ class SetOTPDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(activity) builder.apply { - setTitle(R.string.entry_setup_otp) setView(root) .setPositiveButton(android.R.string.ok) {_, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> } } + root?.findViewById(R.id.otp_information)?.setOnClickListener { + UriUtil.gotoUrl(activity, R.string.otp_explanation_url) + } + return builder.create() } return super.onCreateDialog(savedInstanceState) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt similarity index 97% rename from app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt index 98a975471..ae48ccd03 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.activities +package com.kunzisoft.keepass.activities.fragments import android.content.Context import android.graphics.Color @@ -34,6 +34,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.stylish.StylishFragment @@ -44,7 +45,6 @@ import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.icons.IconDrawableFactory -import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.settings.PreferencesUtil @@ -78,7 +78,7 @@ class EntryEditFragment: StylishFragment() { var drawFactory: IconDrawableFactory? = null var setOnDateClickListener: (() -> Unit)? = null var setOnPasswordGeneratorClickListener: View.OnClickListener? = null - var setOnIconViewClickListener: View.OnClickListener? = null + var setOnIconViewClickListener: ((IconImage) -> Unit)? = null var setOnEditCustomField: ((Field) -> Unit)? = null var setOnRemoveAttachment: ((Attachment) -> Unit)? = null @@ -100,7 +100,7 @@ class EntryEditFragment: StylishFragment() { entryTitleView = rootView.findViewById(R.id.entry_edit_title) entryIconView = rootView.findViewById(R.id.entry_edit_icon_button) entryIconView.setOnClickListener { - setOnIconViewClickListener?.onClick(it) + setOnIconViewClickListener?.invoke(mEntryInfo.icon) } entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name) @@ -239,9 +239,7 @@ class EntryEditFragment: StylishFragment() { } set(value) { mEntryInfo.icon = value - drawFactory?.let { drawFactory -> - entryIconView.assignDatabaseIcon(drawFactory, value, iconColor) - } + drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor) } var username: String @@ -315,7 +313,8 @@ class EntryEditFragment: StylishFragment() { itemView?.id = View.NO_ID val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container) - extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected + extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected) + TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE extraFieldValueContainer?.hint = extraField.name extraFieldValueContainer?.id = View.NO_ID diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt new file mode 100644 index 000000000..281fb5451 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt @@ -0,0 +1,87 @@ +/* + * 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.fragments + +import android.os.Bundle +import android.view.View +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.icon.IconImageCustom + + +class IconCustomFragment : IconFragment() { + + override fun retrieveMainLayoutId(): Int { + return R.layout.fragment_icon_grid + } + + override fun defineIconList() { + mDatabase?.doForEachCustomIcons { customIcon, _ -> + iconPickerAdapter.addIcon(customIcon, false) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected -> + if (customIconsSelected.isEmpty()) { + iconActionSelectionMode = false + iconPickerAdapter.deselectAllIcons() + } else { + iconActionSelectionMode = true + iconPickerAdapter.updateIconSelectedState(customIconsSelected) + } + } + iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustomAdded -> + if (!iconCustomAdded.error) { + iconCustomAdded?.iconCustom?.let { icon -> + iconPickerAdapter.addIcon(icon) + iconCustomAdded.iconCustom = null + } + iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition) + } + } + iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved -> + if (!iconCustomRemoved.error) { + iconCustomRemoved?.iconCustom?.let { icon -> + iconPickerAdapter.removeIcon(icon) + iconCustomRemoved.iconCustom = null + } + } + } + } + + override fun onIconClickListener(icon: IconImageCustom) { + if (iconActionSelectionMode) { + // Same long click behavior after each single click + onIconLongClickListener(icon) + } else { + iconPickerViewModel.pickCustomIcon(icon) + } + } + + override fun onIconLongClickListener(icon: IconImageCustom) { + // Select or deselect item if already selected + icon.selected = !icon.selected + iconPickerAdapter.updateIcon(icon) + iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon() + iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt new file mode 100644 index 000000000..6f067c3cc --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt @@ -0,0 +1,100 @@ +/* + * 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.fragments + +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.adapters.IconPickerAdapter +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.icon.IconImageDraw +import com.kunzisoft.keepass.viewmodels.IconPickerViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +abstract class IconFragment : StylishFragment(), + IconPickerAdapter.IconPickerListener { + + protected lateinit var iconsGridView: RecyclerView + protected lateinit var iconPickerAdapter: IconPickerAdapter + protected var iconActionSelectionMode = false + + protected var mDatabase: Database? = null + + protected val iconPickerViewModel: IconPickerViewModel by activityViewModels() + + abstract fun retrieveMainLayoutId(): Int + + abstract fun defineIconList() + + override fun onAttach(context: Context) { + super.onAttach(context) + + mDatabase = Database.getInstance() + + // Retrieve the textColor to tint the icon + val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) + val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK + ta?.recycle() + + iconPickerAdapter = IconPickerAdapter(context, tintColor).apply { + iconDrawableFactory = mDatabase?.iconDrawableFactory + } + + CoroutineScope(Dispatchers.IO).launch { + val populateList = launch { + iconPickerAdapter.clear() + defineIconList() + } + withContext(Dispatchers.Main) { + populateList.join() + iconPickerAdapter.notifyDataSetChanged() + } + } + } + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View { + val root = inflater.inflate(retrieveMainLayoutId(), container, false) + iconsGridView = root.findViewById(R.id.icons_grid_view) + iconsGridView.adapter = iconPickerAdapter + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + iconPickerAdapter.iconPickerListener = this + } + + fun onIconDeleteClicked() { + iconActionSelectionMode = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt new file mode 100644 index 000000000..ba14e7dea --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt @@ -0,0 +1,77 @@ +package com.kunzisoft.keepass.activities.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.viewmodels.IconPickerViewModel + +class IconPickerFragment : StylishFragment() { + + private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null + private lateinit var viewPager: ViewPager2 + + private val iconPickerViewModel: IconPickerViewModel by activityViewModels() + + private var mDatabase: Database? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_icon_picker, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + mDatabase = Database.getInstance() + + viewPager = view.findViewById(R.id.icon_picker_pager) + val tabLayout = view.findViewById(R.id.icon_picker_tabs) + iconPickerPagerAdapter = IconPickerPagerAdapter(this, + if (mDatabase?.allowCustomIcons == true) 2 else 1) + viewPager.adapter = iconPickerPagerAdapter + TabLayoutMediator(tabLayout, viewPager) { tab, position -> + tab.text = when (position) { + 1 -> getString(R.string.icon_section_custom) + else -> getString(R.string.icon_section_standard) + } + }.attach() + + arguments?.apply { + if (containsKey(ICON_TAB_ARG)) { + viewPager.currentItem = getInt(ICON_TAB_ARG) + } + remove(ICON_TAB_ARG) + } + + iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ -> + viewPager.currentItem = 1 + } + } + + enum class IconTab { + STANDARD, CUSTOM + } + + companion object { + + private const val ICON_TAB_ARG = "ICON_TAB_ARG" + + fun getInstance(iconTab: IconTab): IconPickerFragment { + val fragment = IconPickerFragment() + fragment.arguments = Bundle().apply { + putInt(ICON_TAB_ARG, iconTab.ordinal) + } + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt new file mode 100644 index 000000000..62807e174 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt @@ -0,0 +1,43 @@ +/* + * 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.fragments + +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.icon.IconImageStandard + + +class IconStandardFragment : IconFragment() { + + override fun retrieveMainLayoutId(): Int { + return R.layout.fragment_icon_grid + } + + override fun defineIconList() { + mDatabase?.doForEachStandardIcons { standardIcon -> + iconPickerAdapter.addIcon(standardIcon, false) + } + } + + override fun onIconClickListener(icon: IconImageStandard) { + iconPickerViewModel.pickStandardIcon(icon) + } + + override fun onIconLongClickListener(icon: IconImageStandard) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/ListNodesFragment.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/fragments/ListNodesFragment.kt index 63deba4ef..8153bc336 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/ListNodesFragment.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.activities +package com.kunzisoft.keepass.activities.fragments import android.content.Context import android.content.Intent @@ -28,6 +28,7 @@ import androidx.appcompat.view.ActionMode import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper 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 174904509..4d6406fab 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 @@ -20,11 +20,11 @@ package com.kunzisoft.keepass.activities.stylish import android.content.Context -import androidx.annotation.StyleRes -import androidx.preference.PreferenceManager +import android.content.res.Configuration import android.util.Log - +import androidx.annotation.StyleRes import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.settings.PreferencesUtil /** * Class that provides functions to retrieve and assign a theme to a module @@ -38,17 +38,58 @@ object Stylish { * @param context Context to retrieve the theme preference */ fun init(context: Context) { - val stylishPrefKey = context.getString(R.string.setting_style_key) Log.d(Stylish::class.java.name, "Attatching to " + context.packageName) - themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light)) + themeString = PreferencesUtil.getStyle(context) + } + + private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String { + val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) { + context.getString(R.string.list_style_brightness_light) -> false + context.getString(R.string.list_style_brightness_night) -> true + else -> { + when (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) { + Configuration.UI_MODE_NIGHT_YES -> true + else -> false + } + } + } + return if (systemNightMode) { + retrieveEquivalentNightStyle(context, styleString) + } else { + retrieveEquivalentLightStyle(context, styleString) + } + } + + fun retrieveEquivalentLightStyle(context: Context, styleString: String): String { + return when (styleString) { + context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light) + context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white) + context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear) + context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue) + context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red) + context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple) + else -> styleString + } + } + + private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String { + return when (styleString) { + context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night) + context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black) + context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark) + context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night) + context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night) + context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark) + else -> styleString + } } /** * Assign the style to the class attribute * @param styleString Style id String */ - fun assignStyle(styleString: String) { - themeString = styleString + fun assignStyle(context: Context, styleString: String) { + themeString = retrieveEquivalentSystemStyle(context, styleString) } /** @@ -58,13 +99,16 @@ object Stylish { */ @StyleRes fun getThemeId(context: Context): Int { - - return when (themeString) { + return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) { context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night + context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black + context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue + context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red + context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night 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/activities/stylish/StylishFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt index 6337f1837..8d9aef137 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt @@ -42,6 +42,7 @@ abstract class StylishFragment : Fragment() { contextThemed = ContextThemeWrapper(context, themeId) } + @Suppress("DEPRECATION") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // To fix status bar color if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -53,14 +54,21 @@ abstract class StylishFragment : Fragment() { window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor taStatusBarColor?.recycle() } catch (e: Exception) {} - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) + if (taWindowStatusLight?.getBoolean(0, false) == true) { + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + taWindowStatusLight?.recycle() + } catch (e: Exception) {} + } try { val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor)) window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor taNavigationBarColor?.recycle() } catch (e: Exception) {} } - return super.onCreateView(inflater, container, savedInstanceState) } 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 904aac8b6..31d03ec02 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryAttachmentsItemsAdapter.kt @@ -32,13 +32,14 @@ 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.tasks.BinaryDatabaseManager import com.kunzisoft.keepass.view.expand +import kotlin.math.max class EntryAttachmentsItemsAdapter(context: Context) @@ -48,6 +49,11 @@ class EntryAttachmentsItemsAdapter(context: Context) var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null + // Approximately + private val mImagePreviewMaxWidth = max( + context.resources.displayMetrics.widthPixels, + context.resources.getDimensionPixelSize(R.dimen.item_file_info_height) + ) private var mTitleColor: Int init { @@ -76,7 +82,11 @@ class EntryAttachmentsItemsAdapter(context: Context) if (entryAttachmentState.previewState == AttachmentState.NULL) { entryAttachmentState.previewState = AttachmentState.IN_PROGRESS // Load the bitmap image - Attachment.loadBitmap(entryAttachmentState.attachment, binaryCipherKey) { imageLoaded -> + BinaryDatabaseManager.loadBitmap( + entryAttachmentState.attachment.binaryData, + binaryCipherKey, + mImagePreviewMaxWidth + ) { imageLoaded -> if (imageLoaded == null) { entryAttachmentState.previewState = AttachmentState.ERROR visibility = View.GONE @@ -101,22 +111,22 @@ class EntryAttachmentsItemsAdapter(context: Context) } holder.binaryFileBroken.apply { setColorFilter(Color.RED) - visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { + visibility = if (entryAttachmentState.attachment.binaryData.isCorrupted) { View.VISIBLE } else { View.GONE } } holder.binaryFileTitle.text = entryAttachmentState.attachment.name - if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { + if (entryAttachmentState.attachment.binaryData.isCorrupted) { holder.binaryFileTitle.setTextColor(Color.RED) } else { holder.binaryFileTitle.setTextColor(mTitleColor) } holder.binaryFileSize.text = Formatter.formatFileSize(context, - entryAttachmentState.attachment.binaryAttachment.length) + entryAttachmentState.attachment.binaryData.getSize()) holder.binaryFileCompression.apply { - if (entryAttachmentState.attachment.binaryAttachment.isCompressed) { + if (entryAttachmentState.attachment.binaryData.isCompressed) { text = CompressionAlgorithm.GZip.getName(context.resources) visibility = View.VISIBLE } else { diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt new file mode 100644 index 000000000..4508f5c21 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt @@ -0,0 +1,121 @@ +package com.kunzisoft.keepass.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.icon.IconImageDraw +import com.kunzisoft.keepass.icons.IconDrawableFactory + +class IconPickerAdapter(val context: Context, private val tintIcon: Int) + : RecyclerView.Adapter.CustomIconViewHolder>() { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + + private val iconList = ArrayList() + + var iconDrawableFactory: IconDrawableFactory? = null + var iconPickerListener: IconPickerListener? = null + + val lastPosition: Int + get() = iconList.lastIndex + + fun addIcon(icon: I, notify: Boolean = true) { + if (!iconList.contains(icon)) { + iconList.add(icon) + if (notify) { + notifyItemInserted(iconList.indexOf(icon)) + } + } + } + + fun updateIcon(icon: I) { + val index = iconList.indexOf(icon) + if (index != -1) { + iconList[index] = icon + notifyItemChanged(index) + } + } + + fun updateIconSelectedState(icons: List) { + icons.forEach { icon -> + val index = iconList.indexOf(icon) + if (index != -1 + && iconList[index].selected != icon.selected) { + iconList[index] = icon + notifyItemChanged(index) + } + } + } + + fun removeIcon(icon: I) { + if (iconList.contains(icon)) { + val position = iconList.indexOf(icon) + iconList.remove(icon) + notifyItemRemoved(position) + } + } + + fun containsAnySelectedIcon(): Boolean { + return iconList.firstOrNull { it.selected } != null + } + + fun deselectAllIcons() { + iconList.forEachIndexed { index, icon -> + if (icon.selected) { + icon.selected = false + notifyItemChanged(index) + } + } + } + + fun getSelectedIcons(): List { + return iconList.filter { it.selected } + } + + fun clear() { + iconList.clear() + } + + fun setList(icons: List) { + iconList.clear() + icons.forEach { iconImage -> + iconList.add(iconImage) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomIconViewHolder { + val view = inflater.inflate(R.layout.item_icon, parent, false) + return CustomIconViewHolder(view) + } + + override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) { + val icon = iconList[position] + iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon) + holder.iconContainerView.isSelected = icon.selected + holder.itemView.setOnClickListener { + iconPickerListener?.onIconClickListener(icon) + } + holder.itemView.setOnLongClickListener { + iconPickerListener?.onIconLongClickListener(icon) + true + } + } + + override fun getItemCount(): Int { + return iconList.size + } + + interface IconPickerListener { + fun onIconClickListener(icon: I) + fun onIconLongClickListener(icon: I) + } + + inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container) + var iconImageView: ImageView = itemView.findViewById(R.id.icon_image) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt new file mode 100644 index 000000000..e06ab0c86 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt @@ -0,0 +1,24 @@ +package com.kunzisoft.keepass.adapters + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.kunzisoft.keepass.activities.fragments.IconCustomFragment +import com.kunzisoft.keepass.activities.fragments.IconStandardFragment + +class IconPickerPagerAdapter(fragment: Fragment, val size: Int) + : FragmentStateAdapter(fragment) { + + private val iconStandardFragment = IconStandardFragment() + private val iconCustomFragment = IconCustomFragment() + + override fun getItemCount(): Int { + return size + } + + override fun createFragment(position: Int): Fragment { + return when (position) { + 1 -> iconCustomFragment + else -> iconStandardFragment + } + } +} \ No newline at end of file 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 f68ba9ea7..4487bd776 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -28,6 +28,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedListAdapterCallback @@ -39,7 +40,6 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.Type -import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.strikeOut @@ -100,9 +100,7 @@ class NodeAdapter (private val context: Context) this.mDatabase = Database.getInstance() // Color of content selection - val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) - this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE) - taContentSelectionColor.recycle() + this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) // Retrieve the color to tint the icon val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) @@ -305,7 +303,7 @@ class NodeAdapter (private val context: Context) } holder.imageIdentifier?.setColorFilter(iconColor) holder.icon.apply { - assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor) + mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) // Relative size of the icon layoutParams?.apply { height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt index 4b0d92f53..14d2ce0d0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt @@ -36,7 +36,6 @@ import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.search.SearchHelper -import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.strikeOut @@ -81,10 +80,9 @@ class SearchEntryCursorAdapter(private val context: Context, val viewHolder = view.tag as ViewHolder // Assign image - viewHolder.imageViewIcon?.assignDatabaseIcon( - database.drawFactory, - currentEntry.icon, - iconColor) + viewHolder.imageViewIcon?.let { iconView -> + database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor) + } // Assign title viewHolder.textViewTitle?.apply { @@ -110,10 +108,24 @@ class SearchEntryCursorAdapter(private val context: Context, return database.createEntry()?.apply { database.startManageEntry(this) entryKDB?.let { entryKDB -> - (cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory) + (cursor as EntryCursorKDB).populateEntry(entryKDB, + { standardIconId -> + database.getStandardIcon(standardIconId) + }, + { customIconId -> + database.getCustomIcon(customIconId) + } + ) } entryKDBX?.let { entryKDBX -> - (cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory) + (cursor as EntryCursorKDBX).populateEntry(entryKDBX, + { standardIconId -> + database.getStandardIcon(standardIconId) + }, + { customIconId -> + database.getCustomIcon(customIconId) + } + ) } database.stopManageEntry(this) } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index aba7353a8..db75561ed 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -46,8 +46,6 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.icons.assignDatabaseIcon -import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.AutofillSettingsActivity @@ -86,6 +84,24 @@ object AutofillHelper { return "" } + private fun newRemoteViews(context: Context, + remoteViewsText: String, + remoteViewsIcon: IconImage? = null): RemoteViews { + val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry) + presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText) + if (remoteViewsIcon != null) { + try { + Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context, + remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> + presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap) + } + } catch (e: Exception) { + Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) + } + } + return presentation + } + private fun buildDataset(context: Context, entryInfo: EntryInfo, struct: StructureParser.Result, @@ -116,6 +132,21 @@ object AutofillHelper { } } + /** + * Method to assign a drawable to a new icon from a database icon + */ + private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? { + try { + Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context, + entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> + return Icon.createWithBitmap(bitmap) + } + } catch (e: Exception) { + Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) + } + return null + } + @RequiresApi(Build.VERSION_CODES.R) @SuppressLint("RestrictedApi") private fun buildInlinePresentationForEntry(context: Context, @@ -267,26 +298,4 @@ object AutofillHelper { activity.finish() } } - - private fun newRemoteViews(context: Context, - remoteViewsText: String, - remoteViewsIcon: IconImage? = null): RemoteViews { - val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry) - presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText) - if (remoteViewsIcon != null) { - presentation.assignDatabaseIcon(context, - R.id.autofill_entry_icon, - Database.getInstance().drawFactory, - remoteViewsIcon, - ContextCompat.getColor(context, R.color.green)) - } - return presentation - } - - private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? { - return createIconFromDatabaseIcon(context, - Database.getInstance().drawFactory, - entryInfo.icon, - ContextCompat.getColor(context, R.color.green)) - } } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt index cdd6e11a6..8f093e18d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.autofill +import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.graphics.BlendMode @@ -130,6 +131,7 @@ class KeeAutofillService : AutofillService() { ) } + @SuppressLint("RestrictedApi") private fun showUIForEntrySelection(parseResult: StructureParser.Result, searchInfo: SearchInfo, inlineSuggestionsRequest: InlineSuggestionsRequest?, 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 33e4c4def..84e7829cc 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 @@ -39,6 +39,7 @@ class ReloadDatabaseRunnable(private val context: Context, tempCipherKey = mDatabase.loadedCipherKey // Clear before we load mDatabase.clear(UriUtil.getBinaryDir(context)) + mDatabase.wasReloaded = true } override fun onActionRun() { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt index 673122e6a..dbdaba94e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt @@ -52,16 +52,9 @@ class CopyNodesRunnable constructor( if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) { // Update entry with new values mNewParent.touch(modified = false, touchParents = true) - val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent) - if (entryCopied != null) { - entryCopied.touch(modified = true, touchParents = true) - mEntriesCopied.add(entryCopied) - } else { - Log.e(TAG, "Unable to create a copy of the entry") - setError(CopyEntryDatabaseException()) - break@foreachNode - } + entryCopied.touch(modified = true, touchParents = true) + mEntriesCopied.add(entryCopied) } else { // Only finish thread setError(CopyEntryDatabaseException()) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt index c3a0c002f..e9102a69d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt @@ -65,7 +65,7 @@ class DeleteNodesRunnable(context: Context, database.deleteEntry(currentNode) } // Remove the oldest attachments - currentNode.getAttachments(database.binaryPool).forEach { + currentNode.getAttachments(database.attachmentPool).forEach { database.removeAttachmentIfNotUsed(it) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt index 0f211332a..565702de7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt @@ -42,14 +42,14 @@ class UpdateEntryRunnable constructor( mNewEntry.addParentFrom(mOldEntry) // Build oldest attachments - val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true) - val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true) + val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true) + val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true) val attachmentsToRemove = ArrayList(oldEntryAttachments) // Not use equals because only check name newEntryAttachments.forEach { newAttachment -> oldEntryAttachments.forEach { oldAttachment -> if (oldAttachment.name == newAttachment.name - && oldAttachment.binaryAttachment == newAttachment.binaryAttachment) + && oldAttachment.binaryData == newAttachment.binaryData) attachmentsToRemove.remove(oldAttachment) } } @@ -60,7 +60,7 @@ class UpdateEntryRunnable constructor( // Create an entry history (an entry history don't have history) mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false)) - database.removeOldestEntryHistory(mOldEntry, database.binaryPool) + database.removeOldestEntryHistory(mOldEntry, database.attachmentPool) // Only change data in index database.updateEntry(mOldEntry) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt index 4007d0bdd..84c1eaffb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt @@ -23,7 +23,9 @@ import android.database.MatrixCursor import android.provider.BaseColumns import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.entry.EntryVersioned -import com.kunzisoft.keepass.database.element.icon.IconImageFactory +import com.kunzisoft.keepass.database.element.icon.IconImage +import com.kunzisoft.keepass.database.element.icon.IconImageCustom +import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.NodeId import java.util.* @@ -49,12 +51,16 @@ abstract class EntryCursor> abstract fun getPwNodeId(): NodeId - open fun populateEntry(pwEntry: PwEntryV, iconFactory: IconImageFactory) { + open fun populateEntry(pwEntry: PwEntryV, + retrieveStandardIcon: (Int) -> IconImageStandard, + retrieveCustomIcon: (UUID) -> IconImageCustom) { pwEntry.nodeId = getPwNodeId() pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE)) - val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD))) - pwEntry.icon = iconStandard + val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD))) + val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)), + getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS)))) + pwEntry.icon = IconImage(iconStandard, iconCustom) pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME)) pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD)) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDB.kt index df9eaaf8e..6debdd427 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDB.kt @@ -19,7 +19,6 @@ */ package com.kunzisoft.keepass.database.cursor -import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.entry.EntryKDB class EntryCursorKDB : EntryCursorUUID() { @@ -30,9 +29,9 @@ class EntryCursorKDB : EntryCursorUUID() { entry.id.mostSignificantBits, entry.id.leastSignificantBits, entry.title, - entry.icon.iconId, - DatabaseVersioned.UUID_ZERO.mostSignificantBits, - DatabaseVersioned.UUID_ZERO.leastSignificantBits, + entry.icon.standard.id, + entry.icon.custom.uuid.mostSignificantBits, + entry.icon.custom.uuid.leastSignificantBits, entry.username, entry.password, entry.url, diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt index 8564ca467..a6d8bf342 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt @@ -20,9 +20,9 @@ package com.kunzisoft.keepass.database.cursor import com.kunzisoft.keepass.database.element.entry.EntryKDBX -import com.kunzisoft.keepass.database.element.icon.IconImageFactory - -import java.util.UUID +import com.kunzisoft.keepass.database.element.icon.IconImageCustom +import com.kunzisoft.keepass.database.element.icon.IconImageStandard +import java.util.* class EntryCursorKDBX : EntryCursorUUID() { @@ -34,9 +34,9 @@ class EntryCursorKDBX : EntryCursorUUID() { entry.id.mostSignificantBits, entry.id.leastSignificantBits, entry.title, - entry.icon.iconId, - entry.iconCustom.uuid.mostSignificantBits, - entry.iconCustom.uuid.leastSignificantBits, + entry.icon.standard.id, + entry.icon.custom.uuid.mostSignificantBits, + entry.icon.custom.uuid.leastSignificantBits, entry.username, entry.password, entry.url, @@ -52,14 +52,10 @@ class EntryCursorKDBX : EntryCursorUUID() { entryId++ } - override fun populateEntry(pwEntry: EntryKDBX, iconFactory: IconImageFactory) { - super.populateEntry(pwEntry, iconFactory) - - // Retrieve custom icon - val iconCustom = iconFactory.getIcon( - UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)), - getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS)))) - pwEntry.iconCustom = iconCustom + override fun populateEntry(pwEntry: EntryKDBX, + retrieveStandardIcon: (Int) -> IconImageStandard, + retrieveCustomIcon: (UUID) -> IconImageCustom) { + super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon) // Retrieve extra fields if (extraFieldCursor.moveToFirst()) { 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 9c200d372..cfc4690b5 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,24 +19,23 @@ */ 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.* +import com.kunzisoft.keepass.database.element.database.BinaryByte +import com.kunzisoft.keepass.database.element.database.BinaryData + data class Attachment(var name: String, - var binaryAttachment: BinaryAttachment) : Parcelable { + var binaryData: BinaryData) : Parcelable { constructor(parcel: Parcel) : this( parcel.readString() ?: "", - parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment() + parcel.readParcelable(BinaryData::class.java.classLoader) ?: BinaryByte() ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(name) - parcel.writeParcelable(binaryAttachment, flags) + parcel.writeParcelable(binaryData, flags) } override fun describeContents(): Int { @@ -44,7 +43,7 @@ data class Attachment(var name: String, } override fun toString(): String { - return "$name at $binaryAttachment" + return "$name at $binaryData" } override fun equals(other: Any?): Boolean { @@ -68,28 +67,5 @@ 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 660ae0d16..2258417fb 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 @@ -26,7 +26,9 @@ import android.util.Log import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.element.database.* -import com.kunzisoft.keepass.database.element.icon.IconImageFactory +import com.kunzisoft.keepass.database.element.icon.IconImageCustom +import com.kunzisoft.keepass.database.element.icon.IconImageStandard +import com.kunzisoft.keepass.database.element.icon.IconsManager import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID @@ -51,7 +53,6 @@ import java.security.Key import java.security.SecureRandom import java.util.* import javax.crypto.KeyGenerator -import javax.crypto.spec.IvParameterSpec import kotlin.collections.ArrayList @@ -68,7 +69,10 @@ class Database { var isReadOnly = false - val drawFactory = IconDrawableFactory() + val iconDrawableFactory = IconDrawableFactory( + { loadedCipherKey }, + { iconId -> iconsManager.getBinaryForCustomIcon(iconId) } + ) var loaded = false set(value) { @@ -76,6 +80,11 @@ class Database { loadTimestamp = if (field) System.currentTimeMillis() else null } + /** + * To reload the main activity + */ + var wasReloaded = false + var loadTimestamp: Long? = null private set @@ -92,11 +101,44 @@ class Database { return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey } - val iconFactory: IconImageFactory + private val iconsManager: IconsManager get() { - return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory() + return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager() } + fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) { + return iconsManager.doForEachStandardIcon(action) + } + + fun getStandardIcon(iconId: Int): IconImageStandard { + return iconsManager.getIcon(iconId) + } + + val allowCustomIcons: Boolean + get() = mDatabaseKDBX != null + + fun doForEachCustomIcons(action: (IconImageCustom, BinaryData) -> Unit) { + return iconsManager.doForEachCustomIcon(action) + } + + fun getCustomIcon(iconId: UUID): IconImageCustom { + return iconsManager.getIcon(iconId) + } + + fun buildNewCustomIcon(cacheDirectory: File, + result: (IconImageCustom?, BinaryData?) -> Unit) { + mDatabaseKDBX?.buildNewCustomIcon(cacheDirectory, null, result) + } + + fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { + return mDatabaseKDBX?.isCustomIconBinaryDuplicate(binaryData) ?: false + } + + fun removeCustomIcon(customIcon: IconImageCustom) { + iconDrawableFactory.clearFromCache(customIcon) + iconsManager.removeCustomIcon(customIcon.uuid) + } + val allowName: Boolean get() = mDatabaseKDBX != null @@ -532,9 +574,10 @@ class Database { }, omitBackup, max) } - val binaryPool: BinaryPool + val attachmentPool: AttachmentPool get() { - return mDatabaseKDBX?.binaryPool ?: BinaryPool() + // Binary pool is functionally only in KDBX + return mDatabaseKDBX?.binaryPool ?: AttachmentPool() } val allowMultipleAttachments: Boolean @@ -546,17 +589,17 @@ class Database { return false } - fun buildNewBinary(cacheDirectory: File, - compressed: Boolean = false, - protected: Boolean = false): BinaryAttachment? { - return mDatabaseKDB?.buildNewBinary(cacheDirectory) - ?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected) + fun buildNewBinaryAttachment(cacheDirectory: File, + compressed: Boolean = false, + protected: Boolean = false): BinaryData? { + return mDatabaseKDB?.buildNewAttachment(cacheDirectory) + ?: mDatabaseKDBX?.buildNewAttachment(cacheDirectory, compressed, protected) } fun removeAttachmentIfNotUsed(attachment: Attachment) { // No need in KDB database because unique attachment by entry // Don't clear to fix upload multiple times - mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false) + mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryData, false) } fun removeUnlinkedAttachments() { @@ -625,7 +668,8 @@ class Database { } fun clear(filesDirectory: File? = null) { - drawFactory.clearCache() + iconsManager.clearCache() + iconDrawableFactory.clearCache() // Delete the cache of the database if present mDatabaseKDB?.clearCache() mDatabaseKDBX?.clearCache() @@ -791,7 +835,7 @@ class Database { * @param entryToCopy * @param newParent */ - fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry? { + fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry { val entryCopied = Entry(entryToCopy, false) entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID() entryCopied.parent = newParent @@ -948,7 +992,7 @@ class Database { rootGroup?.doForEachChildAndForIt( object : NodeHandler() { override fun operate(node: Entry): Boolean { - removeOldestEntryHistory(node, binaryPool) + removeOldestEntryHistory(node, attachmentPool) return true } }, @@ -963,7 +1007,7 @@ class Database { /** * Remove oldest history if more than max items or max memory */ - fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) { + fun removeOldestEntryHistory(entry: Entry, attachmentPool: AttachmentPool) { mDatabaseKDBX?.let { val maxItems = historyMaxItems if (maxItems >= 0) { @@ -977,7 +1021,7 @@ class Database { while (true) { var historySize: Long = 0 for (entryHistory in entry.getHistory()) { - historySize += entryHistory.getSize(binaryPool) + historySize += entryHistory.getSize(attachmentPool) } if (historySize > maxSize) { removeOldestEntryHistory(entry) @@ -991,7 +1035,7 @@ class Database { private fun removeOldestEntryHistory(entry: Entry) { entry.removeOldestEntryFromHistory()?.let { - it.getAttachments(binaryPool, false).forEach { attachmentToRemove -> + it.getAttachments(attachmentPool, false).forEach { attachmentToRemove -> removeAttachmentIfNotUsed(attachmentToRemove) } } @@ -999,7 +1043,7 @@ class Database { fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) { entry.removeEntryFromHistory(entryHistoryPosition)?.let { - it.getAttachments(binaryPool, false).forEach { attachmentToRemove -> + it.getAttachments(attachmentPool, false).forEach { attachmentToRemove -> removeAttachmentIfNotUsed(attachmentToRemove) } } 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 56fcf7b17..1d0eeee1a 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 @@ -21,14 +21,12 @@ package com.kunzisoft.keepass.database.element import android.os.Parcel import android.os.Parcelable -import com.kunzisoft.keepass.database.element.database.BinaryPool +import com.kunzisoft.keepass.database.element.database.AttachmentPool import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.database.element.icon.IconImageCustom -import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdUUID @@ -109,7 +107,7 @@ class Entry : Node, EntryVersionedInterface { override var icon: IconImage get() { - return entryKDB?.icon ?: entryKDBX?.icon ?: IconImageStandard() + return entryKDB?.icon ?: entryKDBX?.icon ?: IconImage() } set(value) { entryKDB?.icon = value @@ -257,31 +255,12 @@ class Entry : Node, EntryVersionedInterface { } } - /* - ------------ - KDB Methods - ------------ - */ - - /** - * If it's a node with only meta information like Meta-info SYSTEM Database Color - * @return false by default, true if it's a meta stream - */ - val isMetaStream: Boolean - get() = entryKDB?.isMetaStream ?: false - /* ------------ KDBX Methods ------------ */ - var iconCustom: IconImageCustom - get() = entryKDBX?.iconCustom ?: IconImageCustom.UNKNOWN_ICON - set(value) { - entryKDBX?.iconCustom = value - } - /** * Retrieve extra fields to show, key is the label, value is the value of field (protected or not) * @return Map of label/value @@ -330,12 +309,12 @@ class Entry : Node, EntryVersionedInterface { entryKDBX?.stopToManageFieldReferences() } - fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List { + fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List { val attachments = ArrayList() entryKDB?.getAttachment()?.let { attachments.add(it) } - entryKDBX?.getAttachments(binaryPool, inHistory)?.let { + entryKDBX?.getAttachments(attachmentPool, inHistory)?.let { attachments.addAll(it) } return attachments @@ -356,9 +335,9 @@ class Entry : Node, EntryVersionedInterface { entryKDBX?.removeAttachments() } - private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { + private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) { entryKDB?.putAttachment(attachment) - entryKDBX?.putAttachment(attachment, binaryPool) + entryKDBX?.putAttachment(attachment, attachmentPool) } fun getHistory(): ArrayList { @@ -390,8 +369,8 @@ class Entry : Node, EntryVersionedInterface { return null } - fun getSize(binaryPool: BinaryPool): Long { - return entryKDBX?.getSize(binaryPool) ?: 0L + fun getSize(attachmentPool: AttachmentPool): Long { + return entryKDBX?.getSize(attachmentPool) ?: 0L } fun containsCustomData(): Boolean { @@ -433,7 +412,7 @@ class Entry : Node, EntryVersionedInterface { // Replace parameter fields by generated OTP fields entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) } - database?.binaryPool?.let { binaryPool -> + database?.attachmentPool?.let { binaryPool -> entryInfo.attachments = getAttachments(binaryPool) } @@ -460,7 +439,7 @@ class Entry : Node, EntryVersionedInterface { url = newEntryInfo.url notes = newEntryInfo.notes addExtraFields(newEntryInfo.customFields) - database?.binaryPool?.let { binaryPool -> + database?.attachmentPool?.let { binaryPool -> newEntryInfo.attachments.forEach { attachment -> putAttachment(attachment, binaryPool) } 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 3478376db..f5f06332a 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 @@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface 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 @@ -41,6 +40,9 @@ class Group : Node, GroupVersionedInterface { var groupKDBX: GroupKDBX? = null private set + // Virtual group is used to defined a detached database group + var isVirtual = false + fun updateWith(group: Group) { group.groupKDB?.let { this.groupKDB?.updateWith(it) @@ -78,6 +80,7 @@ class Group : Node, GroupVersionedInterface { constructor(parcel: Parcel) { groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader) groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader) + isVirtual = parcel.readByte().toInt() != 0 } enum class ChildFilter { @@ -111,6 +114,7 @@ class Group : Node, GroupVersionedInterface { override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeParcelable(groupKDB, flags) dest.writeParcelable(groupKDBX, flags) + dest.writeByte((if (isVirtual) 1 else 0).toByte()) } override val nodeId: NodeId<*>? @@ -124,7 +128,7 @@ class Group : Node, GroupVersionedInterface { } override var icon: IconImage - get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImageStandard() + get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImage() set(value) { groupKDB?.icon = value groupKDBX?.icon = value @@ -344,9 +348,11 @@ class Group : Node, GroupVersionedInterface { groupKDBX?.removeChildren() } - override fun allowAddEntryIfIsRoot(): Boolean { - return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false - } + val allowAddEntryIfIsRoot: Boolean + get() = groupKDBX != null + + val allowAddNoteInGroup: Boolean + get() = groupKDBX != null /* ------------ diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/AttachmentPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/AttachmentPool.kt new file mode 100644 index 000000000..dc94e31c2 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/AttachmentPool.kt @@ -0,0 +1,44 @@ +/* + * 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.database.element.database + +class AttachmentPool : BinaryPool() { + + /** + * Utility method to find an unused key in the pool + */ + override fun findUnusedKey(): Int { + var unusedKey = 0 + while (pool[unusedKey] != null) + unusedKey++ + return unusedKey + } + + /** + * To register a binary with a ref corresponding to an ordered index + */ + fun getBinaryIndexFromKey(key: Int): Int? { + val index = orderedBinariesWithoutDuplication().indexOfFirst { it.keys.contains(key) } + return if (index < 0) + null + else + index + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryByte.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryByte.kt new file mode 100644 index 000000000..c127e7301 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryByte.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2018 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.database.element.database + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.stream.readAllBytes +import java.io.* +import java.util.zip.GZIPOutputStream + +class BinaryByte : BinaryData { + + private var mDataByte: ByteArray = ByteArray(0) + + /** + * Empty protected binary + */ + constructor() : super() + + constructor(byteArray: ByteArray, + compressed: Boolean = false, + protected: Boolean = false) : super(compressed, protected) { + this.mDataByte = byteArray + } + + constructor(parcel: Parcel) : super(parcel) { + val byteArray = ByteArray(parcel.readInt()) + parcel.readByteArray(byteArray) + mDataByte = byteArray + } + + @Throws(IOException::class) + override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream { + return ByteArrayInputStream(mDataByte) + } + + @Throws(IOException::class) + override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream { + return ByteOutputStream() + } + + @Throws(IOException::class) + override fun compress(cipherKey: Database.LoadedKey) { + if (!isCompressed) { + GZIPOutputStream(getOutputDataStream(cipherKey)).use { outputStream -> + getInputDataStream(cipherKey).use { inputStream -> + inputStream.readAllBytes { buffer -> + outputStream.write(buffer) + } + } + isCompressed = true + } + } + } + + @Throws(IOException::class) + override fun decompress(cipherKey: Database.LoadedKey) { + if (isCompressed) { + getUnGzipInputDataStream(cipherKey).use { inputStream -> + getOutputDataStream(cipherKey).use { outputStream -> + inputStream.readAllBytes { buffer -> + outputStream.write(buffer) + } + } + isCompressed = false + } + } + } + + @Throws(IOException::class) + override fun clear() { + mDataByte = ByteArray(0) + } + + override fun dataExists(): Boolean { + return mDataByte.isNotEmpty() + } + + override fun getSize(): Long { + return mDataByte.size.toLong() + } + + /** + * Hash of the raw encrypted file in temp folder, only to compare binary data + */ + override fun binaryHash(): Int { + return if (dataExists()) + mDataByte.contentHashCode() + else + 0 + } + + override fun toString(): String { + return mDataByte.toString() + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + super.writeToParcel(dest, flags) + dest.writeInt(mDataByte.size) + dest.writeByteArray(mDataByte) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BinaryByte) return false + if (!super.equals(other)) return false + + if (!mDataByte.contentEquals(other.mDataByte)) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + mDataByte.contentHashCode() + return result + } + + /** + * Custom OutputStream to calculate the size and hash of binary file + */ + private inner class ByteOutputStream : ByteArrayOutputStream() { + override fun close() { + mDataByte = this.toByteArray() + super.close() + } + } + + companion object { + + private val TAG = BinaryByte::class.java.name + const val MAX_BINARY_BYTES = 10240 + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): BinaryByte { + return BinaryByte(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryData.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryData.kt new file mode 100644 index 000000000..c51493789 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryData.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2018 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.database.element.database + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.database.element.Database +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +abstract class BinaryData : Parcelable { + + var isCompressed: Boolean = false + protected set + var isProtected: Boolean = false + protected set + var isCorrupted: Boolean = false + + /** + * Empty protected binary + */ + protected constructor() + + protected constructor(compressed: Boolean = false, protected: Boolean = false) { + this.isCompressed = compressed + this.isProtected = protected + } + + protected constructor(parcel: Parcel) { + isCompressed = parcel.readByte().toInt() != 0 + isProtected = parcel.readByte().toInt() != 0 + isCorrupted = parcel.readByte().toInt() != 0 + } + + @Throws(IOException::class) + abstract fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream + + @Throws(IOException::class) + abstract fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream + + @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) + abstract fun compress(cipherKey: Database.LoadedKey) + + @Throws(IOException::class) + abstract fun decompress(cipherKey: Database.LoadedKey) + + @Throws(IOException::class) + abstract fun clear() + + abstract fun dataExists(): Boolean + + abstract fun getSize(): Long + + abstract fun binaryHash(): Int + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeByte((if (isCompressed) 1 else 0).toByte()) + dest.writeByte((if (isProtected) 1 else 0).toByte()) + dest.writeByte((if (isCorrupted) 1 else 0).toByte()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BinaryData) return false + + if (isCompressed != other.isCompressed) return false + if (isProtected != other.isProtected) return false + if (isCorrupted != other.isCorrupted) return false + + return true + } + + override fun hashCode(): Int { + var result = isCompressed.hashCode() + result = 31 * result + isProtected.hashCode() + result = 31 * result + isCorrupted.hashCode() + return result + } + + companion object { + private val TAG = BinaryData::class.java.name + } + +} 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/BinaryFile.kt similarity index 60% rename from app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryAttachment.kt rename to app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryFile.kt index 9d3d5fc45..f3cd61060 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/BinaryFile.kt @@ -28,75 +28,51 @@ 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.nio.ByteBuffer +import java.security.MessageDigest import java.util.zip.GZIPOutputStream import javax.crypto.Cipher import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream import javax.crypto.spec.IvParameterSpec -class BinaryAttachment : Parcelable { +class BinaryFile : BinaryData { - 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 + private var mDataFile: File? = null + private var mLength: Long = 0 + private var mBinaryHash = 0 // Cipher to encrypt temp file + @Transient private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER) + @Transient private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER) - /** - * Empty protected binary - */ - constructor() + constructor() : super() - constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) { - this.dataFile = dataFile - this.length = 0 - this.isCompressed = compressed - this.isProtected = protected + constructor(dataFile: File, + compressed: Boolean = false, + protected: Boolean = false) : super(compressed, protected) { + this.mDataFile = dataFile + this.mLength = 0 + this.mBinaryHash = 0 } - private constructor(parcel: Parcel) { + constructor(parcel: Parcel) : super(parcel) { parcel.readString()?.let { - dataFile = File(it) + mDataFile = 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(cipherKey: Database.LoadedKey): InputStream { - return buildInputStream(dataFile!!, cipherKey) - } - - @Throws(IOException::class) - fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream { - return buildOutputStream(dataFile!!, cipherKey) + mLength = parcel.readLong() + mBinaryHash = parcel.readInt() } @Throws(IOException::class) - fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream { - return if (isCompressed) { - GZIPInputStream(getInputDataStream(cipherKey)) - } else { - getInputDataStream(cipherKey) - } + override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream { + return buildInputStream(mDataFile, cipherKey) } @Throws(IOException::class) - fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream { - return if (isCompressed) { - GZIPOutputStream(getOutputDataStream(cipherKey)) - } else { - getOutputDataStream(cipherKey) - } + override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream { + return buildOutputStream(mDataFile, cipherKey) } @Throws(IOException::class) @@ -122,8 +98,8 @@ class BinaryAttachment : Parcelable { } @Throws(IOException::class) - fun compress(cipherKey: Database.LoadedKey) { - dataFile?.let { concreteDataFile -> + override fun compress(cipherKey: Database.LoadedKey) { + mDataFile?.let { concreteDataFile -> // To compress, create a new binary with file if (!isCompressed) { // Encrypt the new gzipped temp file @@ -147,8 +123,8 @@ class BinaryAttachment : Parcelable { } @Throws(IOException::class) - fun decompress(cipherKey: Database.LoadedKey) { - dataFile?.let { concreteDataFile -> + override fun decompress(cipherKey: Database.LoadedKey) { + mDataFile?.let { concreteDataFile -> if (isCompressed) { // Encrypt the new ungzipped temp file val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") @@ -171,86 +147,105 @@ class BinaryAttachment : Parcelable { } @Throws(IOException::class) - fun clear() { - if (dataFile != null && !dataFile!!.delete()) - throw IOException("Unable to delete temp file " + dataFile!!.absolutePath) + override fun clear() { + if (mDataFile != null && !mDataFile!!.delete()) + throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath) } - override fun equals(other: Any?): Boolean { - if (this === other) - return true - if (other == null || javaClass != other.javaClass) - return false - if (other !is BinaryAttachment) - return false - - var sameData = false - if (dataFile != null && dataFile == other.dataFile) - sameData = true - - return isCompressed == other.isCompressed - && isProtected == other.isProtected - && isCorrupted == other.isCorrupted - && sameData + override fun dataExists(): Boolean { + return mDataFile != null && mLength > 0 } - override fun hashCode(): Int { + override fun getSize(): Long { + return mLength + } - var result = 0 - result = 31 * result + if (isCompressed) 1 else 0 - 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 + /** + * Hash of the raw encrypted file in temp folder, only to compare binary data + */ + @Throws(FileNotFoundException::class) + override fun binaryHash(): Int { + return mBinaryHash } override fun toString(): String { - return dataFile.toString() + return mDataFile.toString() } - override fun describeContents(): Int { - return 0 + override fun writeToParcel(dest: Parcel, flags: Int) { + super.writeToParcel(dest, flags) + dest.writeString(mDataFile?.absolutePath) + dest.writeLong(mLength) + dest.writeInt(mBinaryHash) } - 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()) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BinaryFile) return false + if (!super.equals(other)) return false + + return mDataFile != null && mDataFile == other.mDataFile + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (mDataFile?.hashCode() ?: 0) + result = 31 * result + mLength.hashCode() + result = 31 * result + mBinaryHash + return result } /** - * Custom OutputStream to calculate the size of binary file + * Custom OutputStream to calculate the size and hash of binary file */ private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) { + + private val mMessageDigest: MessageDigest init { - length = 0 + mLength = 0 + mMessageDigest = MessageDigest.getInstance("MD5") + mBinaryHash = 0 } override fun beforeWrite(n: Int) { super.beforeWrite(n) - length = byteCount + mLength = byteCount + } + + override fun write(idx: Int) { + super.write(idx) + mMessageDigest.update(idx.toByte()) + } + + override fun write(bts: ByteArray) { + super.write(bts) + mMessageDigest.update(bts) + } + + override fun write(bts: ByteArray, st: Int, end: Int) { + super.write(bts, st, end) + mMessageDigest.update(bts, st, end) } override fun close() { super.close() - length = byteCount + mLength = byteCount + val bytes = mMessageDigest.digest() + mBinaryHash = ByteBuffer.wrap(bytes).int } } companion object { - private val TAG = BinaryAttachment::class.java.name + private val TAG = BinaryFile::class.java.name @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): BinaryAttachment { - return BinaryAttachment(parcel) + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): BinaryFile { + return BinaryFile(parcel) } - override fun newArray(size: Int): Array { + override fun newArray(size: Int): Array { return arrayOfNulls(size) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryPool.kt index 98346223d..9aaea8e86 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryPool.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/BinaryPool.kt @@ -19,48 +19,78 @@ */ package com.kunzisoft.keepass.database.element.database +import android.util.Log +import java.io.File import java.io.IOException +import kotlin.math.abs -class BinaryPool { - private val pool = LinkedHashMap() +abstract class BinaryPool { + + protected val pool = LinkedHashMap() + + // To build unique file id + private var creationId: String = System.currentTimeMillis().toString() + private var poolId: String = abs(javaClass.simpleName.hashCode()).toString() + private var binaryFileIncrement = 0L /** * To get a binary by the pool key (ref attribute in entry) */ - operator fun get(key: Int): BinaryAttachment? { + operator fun get(key: T): BinaryData? { return pool[key] } + /** + * Create and return a new binary file not yet linked to a binary + */ + fun put(key: T? = null, + builder: (uniqueBinaryId: String) -> BinaryData): KeyBinary { + binaryFileIncrement++ + val newBinaryFile: BinaryData = builder("$poolId$creationId$binaryFileIncrement") + val newKey = put(key, newBinaryFile) + return KeyBinary(newBinaryFile, newKey) + } + /** * To linked a binary with a pool key, if the pool key doesn't exists, create an unused one */ - fun put(key: Int?, value: BinaryAttachment) { + fun put(key: T?, value: BinaryData): T { if (key == null) - put(value) + return put(value) else pool[key] = value + return key } /** - * To put a [binaryAttachment] in the pool, + * To put a [binaryData] in the pool, * if already exists, replace the current one, * else add it with a new key */ - fun put(binaryAttachment: BinaryAttachment): Int { - var key = findKey(binaryAttachment) + fun put(binaryData: BinaryData): T { + var key: T? = findKey(binaryData) if (key == null) { key = findUnusedKey() } - pool[key] = binaryAttachment + pool[key!!] = binaryData return key } + /** + * Remove a binary from the pool with its [key], the file is not deleted + */ + @Throws(IOException::class) + fun remove(key: T) { + pool.remove(key) + // Don't clear attachment here because a file can be used in many BinaryAttachment + } + /** * Remove a binary from the pool, the file is not deleted */ @Throws(IOException::class) - fun remove(binaryAttachment: BinaryAttachment) { - findKey(binaryAttachment)?.let { + fun remove(binaryData: BinaryData) { + findKey(binaryData)?.let { pool.remove(it) } // Don't clear attachment here because a file can be used in many BinaryAttachment @@ -69,23 +99,18 @@ class BinaryPool { /** * Utility method to find an unused key in the pool */ - private fun findUnusedKey(): Int { - var unusedKey = 0 - while (pool[unusedKey] != null) - unusedKey++ - return unusedKey - } + abstract fun findUnusedKey(): T /** - * Return key of [binaryAttachmentToRetrieve] or null if not found + * Return key of [binaryDataToRetrieve] or null if not found */ - private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? { - val contains = pool.containsValue(binaryAttachmentToRetrieve) + private fun findKey(binaryDataToRetrieve: BinaryData): T? { + val contains = pool.containsValue(binaryDataToRetrieve) return if (!contains) null else { for ((key, binary) in pool) { - if (binary == binaryAttachmentToRetrieve) { + if (binary == binaryDataToRetrieve) { return key } } @@ -93,46 +118,116 @@ class BinaryPool { } } + fun isBinaryDuplicate(binaryData: BinaryData?): Boolean { + try { + binaryData?.let { + if (it.getSize() > 0) { + val searchBinaryMD5 = it.binaryHash() + var i = 0 + for ((_, binary) in pool) { + if (binary.binaryHash() == searchBinaryMD5) { + i++ + if (i > 1) + return true + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to check binary duplication", e) + } + return false + } + + /** + * To do an action on each binary in the pool (order is not important) + */ + private fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit, + condition: (key: T, binary: BinaryData) -> Boolean) { + for ((key, value) in pool) { + if (condition.invoke(key, value)) { + action.invoke(key, value) + } + } + } + + fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit) { + doForEachBinary(action) { _, _ -> true } + } + /** * Utility method to order binaries and solve index problem in database v4 */ - private fun orderedBinaries(): List { - val keyBinaryList = ArrayList() + protected fun orderedBinariesWithoutDuplication(condition: ((binary: BinaryData) -> Boolean) = { true }) + : List> { + val keyBinaryList = ArrayList>() for ((key, binary) in pool) { - keyBinaryList.add(KeyBinary(key, binary)) + // Don't deduplicate + val existentBinary = + try { + if (binary.getSize() > 0) { + keyBinaryList.find { + val hash0 = it.binary.binaryHash() + val hash1 = binary.binaryHash() + hash0 != 0 && hash1 != 0 && hash0 == hash1 + } + } else { + null + } + } catch (e: Exception) { + Log.e(TAG, "Unable to check binary hash", e) + null + } + if (existentBinary == null) { + val newKeyBinary = KeyBinary(binary, key) + if (condition.invoke(newKeyBinary.binary)) { + keyBinaryList.add(newKeyBinary) + } + } else { + if (condition.invoke(existentBinary.binary)) { + existentBinary.addKey(key) + } + } } return keyBinaryList } /** - * To register a binary with a ref corresponding to an ordered index + * Different from doForEach, provide an ordered index to each binary */ - fun getBinaryIndexFromKey(key: Int): Int? { - val index = orderedBinaries().indexOfFirst { it.key == key } - return if (index < 0) - null - else - index + private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary) -> Unit, + conditionToAdd: (binary: BinaryData) -> Boolean) { + orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary -> + action.invoke(keyBinary) + } + } + + fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary) -> Unit) { + doForEachBinaryWithoutDuplication(action, { true }) } /** * Different from doForEach, provide an ordered index to each binary */ - fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) { - orderedBinaries().forEachIndexed(action) + private fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit, + conditionToAdd: (binary: BinaryData) -> Boolean) { + orderedBinariesWithoutDuplication(conditionToAdd).forEachIndexed { index, keyBinary -> + action.invoke(index, keyBinary.binary) + } } - /** - * To do an action on each binary in the pool - */ - fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) { - pool.values.forEach { action.invoke(it) } + fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit) { + doForEachOrderedBinaryWithoutDuplication(action, { true }) + } + + fun isEmpty(): Boolean { + return pool.isEmpty() } @Throws(IOException::class) fun clear() { - doForEachBinary { - it.clear() + doForEachBinary { _, binary -> + binary.clear() } pool.clear() } @@ -149,7 +244,20 @@ class BinaryPool { } /** - * Utility data class to order binaries + * Utility class to order binaries */ - data class KeyBinary(val key: Int, val binary: BinaryAttachment) + class KeyBinary(val binary: BinaryData, key: T) { + val keys = HashSet() + init { + addKey(key) + } + + fun addKey(key: T) { + keys.add(key) + } + } + + companion object { + private val TAG = BinaryPool::class.java.name + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/CustomIconPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/CustomIconPool.kt new file mode 100644 index 000000000..167393980 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/CustomIconPool.kt @@ -0,0 +1,14 @@ +package com.kunzisoft.keepass.database.element.database + +import java.util.* + +class CustomIconPool : BinaryPool() { + + override fun findUnusedKey(): UUID { + var newUUID = UUID.randomUUID() + while (pool.containsKey(newUUID)) { + newUUID = UUID.randomUUID() + } + return newUUID + } +} \ No newline at end of file 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 a72875753..96c9555b0 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 @@ -24,6 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.group.GroupKDB +import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeVersioned @@ -44,7 +45,8 @@ class DatabaseKDB : DatabaseVersioned() { private var kdfListV3: MutableList = ArrayList() - private var binaryIncrement = 0 + // Only to generate unique file name + private var binaryPool = AttachmentPool() override val version: String get() = "KeePass 1" @@ -68,7 +70,7 @@ class DatabaseKDB : DatabaseVersioned() { getGroupById(backupGroupId) } - override val kdfEngine: KdfEngine? + override val kdfEngine: KdfEngine get() = kdfListV3[0] override val kdfAvailableList: List @@ -175,6 +177,10 @@ class DatabaseKDB : DatabaseVersioned() { return false } + override fun getStandardIcon(iconId: Int): IconImageStandard { + return this.iconsManager.getIcon(iconId) + } + override fun containsCustomData(): Boolean { return false } @@ -223,7 +229,7 @@ class DatabaseKDB : DatabaseVersioned() { // Create recycle bin val recycleBinGroup = createGroup().apply { title = BACKUP_FOLDER_TITLE - icon = iconFactory.trashIcon + icon.standard = getStandardIcon(IconImageStandard.TRASH_ID) } addGroupTo(recycleBinGroup, rootGroup) backupGroupId = recycleBinGroup.id @@ -269,11 +275,12 @@ class DatabaseKDB : DatabaseVersioned() { addEntryTo(entry, origParent) } - fun buildNewBinary(cacheDirectory: File): BinaryAttachment { + fun buildNewAttachment(cacheDirectory: File): BinaryData { // Generate an unique new file - val fileInCache = File(cacheDirectory, binaryIncrement.toString()) - binaryIncrement++ - return BinaryAttachment(fileInCache) + return binaryPool.put { uniqueBinaryId -> + val fileInCache = File(cacheDirectory, uniqueBinaryId) + BinaryFile(fileInCache) + }.binary } companion object { 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 d2ebabd20..5ebda8474 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 @@ -36,6 +36,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BAC import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.icon.IconImageCustom +import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm @@ -105,11 +106,9 @@ class DatabaseKDBX : DatabaseVersioned { var lastTopVisibleGroupUUID = UUID_ZERO var memoryProtection = MemoryProtectionConfig() val deletedObjects = ArrayList() - val customIcons = ArrayList() val customData = HashMap() - var binaryPool = BinaryPool() - private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast) + var binaryPool = AttachmentPool() var localizedAppName = "KeePassDX" @@ -129,7 +128,7 @@ class DatabaseKDBX : DatabaseVersioned { kdbxVersion = FILE_VERSION_32_3 val group = createGroup().apply { title = rootName - icon = iconFactory.folderIcon + icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID) } rootGroup = group addGroupIndex(group) @@ -211,7 +210,7 @@ class DatabaseKDBX : DatabaseVersioned { } private fun compressAllBinaries() { - binaryPool.doForEachBinary { binary -> + binaryPool.doForEachBinary { _, binary -> try { val cipherKey = loadedCipherKey ?: throw IOException("Unable to retrieve cipher key to compress binaries") @@ -224,7 +223,7 @@ class DatabaseKDBX : DatabaseVersioned { } private fun decompressAllBinaries() { - binaryPool.doForEachBinary { binary -> + binaryPool.doForEachBinary { _, binary -> try { val cipherKey = loadedCipherKey ?: throw IOException("Unable to retrieve cipher key to decompress binaries") @@ -307,16 +306,29 @@ class DatabaseKDBX : DatabaseVersioned { this.dataEngine = dataEngine } - fun getCustomIcons(): List { - return customIcons + override fun getStandardIcon(iconId: Int): IconImageStandard { + return this.iconsManager.getIcon(iconId) } - fun addCustomIcon(customIcon: IconImageCustom) { - this.customIcons.add(customIcon) + fun buildNewCustomIcon(cacheDirectory: File, + customIconId: UUID? = null, + result: (IconImageCustom, BinaryData?) -> Unit) { + iconsManager.buildNewCustomIcon(cacheDirectory, customIconId, result) } - fun getCustomData(): Map { - return customData + fun addCustomIcon(cacheDirectory: File, + customIconId: UUID? = null, + dataSize: Int, + result: (IconImageCustom, BinaryData?) -> Unit) { + iconsManager.addCustomIcon(cacheDirectory, customIconId, dataSize, result) + } + + fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean { + return iconsManager.isCustomIconBinaryDuplicate(binary) + } + + fun getCustomIcon(iconUuid: UUID): IconImageCustom { + return this.iconsManager.getIcon(iconUuid) } fun putCustomData(label: String, value: String) { @@ -324,7 +336,7 @@ class DatabaseKDBX : DatabaseVersioned { } override fun containsCustomData(): Boolean { - return getCustomData().isNotEmpty() + return customData.isNotEmpty() } @Throws(IOException::class) @@ -550,7 +562,7 @@ class DatabaseKDBX : DatabaseVersioned { // Create recycle bin val recycleBinGroup = createGroup().apply { title = resources.getString(R.string.recycle_bin) - icon = iconFactory.trashIcon + icon.standard = getStandardIcon(IconImageStandard.TRASH_ID) enableAutoType = false enableSearching = false isExpanded = false @@ -629,21 +641,18 @@ class DatabaseKDBX : DatabaseVersioned { return publicCustomData.size() > 0 } - fun buildNewBinary(cacheDirectory: File, - compression: Boolean, - protection: Boolean, - binaryPoolId: Int? = null): BinaryAttachment { - // New file with current time - val fileInCache = File(cacheDirectory, binaryIncrement.toString()) - binaryIncrement++ - val binaryAttachment = BinaryAttachment(fileInCache, compression, protection) - // add attachment to pool - binaryPool.put(binaryPoolId, binaryAttachment) - return binaryAttachment + fun buildNewAttachment(cacheDirectory: File, + compression: Boolean, + protection: Boolean, + binaryPoolId: Int? = null): BinaryData { + return binaryPool.put(binaryPoolId) { uniqueBinaryId -> + val fileInCache = File(cacheDirectory, uniqueBinaryId) + BinaryFile(fileInCache, compression, protection) + }.binary } - fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) { - val listBinaries = ArrayList() + fun removeUnlinkedAttachment(binary: BinaryData, clear: Boolean) { + val listBinaries = ArrayList() listBinaries.add(binary) removeUnlinkedAttachments(listBinaries, clear) } @@ -652,11 +661,11 @@ class DatabaseKDBX : DatabaseVersioned { removeUnlinkedAttachments(emptyList(), clear) } - private fun removeUnlinkedAttachments(binaries: List, clear: Boolean) { + private fun removeUnlinkedAttachments(binaries: List, clear: Boolean) { // Build binaries to remove with all binaries known - val binariesToRemove = ArrayList() + val binariesToRemove = ArrayList() if (binaries.isEmpty()) { - binaryPool.doForEachBinary { binary -> + binaryPool.doForEachBinary { _, binary -> binariesToRemove.add(binary) } } else { @@ -666,7 +675,7 @@ class DatabaseKDBX : DatabaseVersioned { rootGroup?.doForEachChild(object : NodeHandler() { override fun operate(node: EntryKDBX): Boolean { node.getAttachments(binaryPool, true).forEach { - binariesToRemove.remove(it.binaryAttachment) + binariesToRemove.remove(it.binaryData) } return binariesToRemove.isNotEmpty() } 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 560d8968e..019126dc4 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 @@ -23,7 +23,8 @@ 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 +import com.kunzisoft.keepass.database.element.icon.IconImageStandard +import com.kunzisoft.keepass.database.element.icon.IconsManager import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm @@ -55,8 +56,13 @@ abstract class DatabaseVersioned< var finalKey: ByteArray? = null protected set - var iconFactory = IconImageFactory() - protected set + /** + * 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 + + val iconsManager = IconsManager() var changeDuplicateId = false @@ -329,6 +335,8 @@ abstract class DatabaseVersioned< abstract fun rootCanContainsEntry(): Boolean + abstract fun getStandardIcon(iconId: Int): IconImageStandard + abstract fun containsCustomData(): Boolean fun addGroupTo(newGroup: Group, parent: Group?) { @@ -384,12 +392,6 @@ 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/EntryKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt index c0d2da8ca..1b2acdee3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt @@ -21,15 +21,15 @@ package com.kunzisoft.keepass.database.element.entry import android.os.Parcel import android.os.Parcelable +import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.database.BinaryData import com.kunzisoft.keepass.database.element.group.GroupKDB +import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeKDBInterface import com.kunzisoft.keepass.database.element.node.Type -import com.kunzisoft.keepass.database.element.database.BinaryAttachment -import com.kunzisoft.keepass.database.element.Attachment import java.util.* -import kotlin.collections.ArrayList /** * Structure containing information about one entry. @@ -56,7 +56,7 @@ class EntryKDB : EntryVersioned, NodeKDBInterface /** A string describing what is in binaryData */ var binaryDescription = "" - var binaryData: BinaryAttachment? = null + var binaryData: BinaryData? = null // Determine if this is a MetaStream entry val isMetaStream: Boolean @@ -68,7 +68,8 @@ class EntryKDB : EntryVersioned, NodeKDBInterface if (username.isEmpty()) return false if (username != PMS_ID_USER) return false if (url.isEmpty()) return false - return if (url != PMS_ID_URL) false else icon.isMetaStreamIcon + if (url != PMS_ID_URL) return false + return icon.standard.id == KEY_ID } override fun initNodeId(): NodeId { @@ -88,7 +89,7 @@ class EntryKDB : EntryVersioned, NodeKDBInterface url = parcel.readString() ?: url notes = parcel.readString() ?: notes binaryDescription = parcel.readString() ?: binaryDescription - binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader) + binaryData = parcel.readParcelable(BinaryData::class.java.classLoader) } override fun readParentParcelable(parcel: Parcel): GroupKDB? { @@ -150,7 +151,7 @@ class EntryKDB : EntryVersioned, NodeKDBInterface fun putAttachment(attachment: Attachment) { this.binaryDescription = attachment.name - this.binaryData = attachment.binaryAttachment + this.binaryData = attachment.binaryData } fun removeAttachment(attachment: Attachment? = null) { 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 f6d877322..c1d0ea8cb 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 @@ -23,12 +23,9 @@ import android.os.Parcel import android.os.Parcelable import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.DateInstant -import com.kunzisoft.keepass.database.element.database.BinaryPool +import com.kunzisoft.keepass.database.element.database.AttachmentPool import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX -import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.database.element.icon.IconImageCustom -import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface @@ -48,19 +45,6 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte @Transient private var mDecodeRef = false - override var icon: IconImage - get() { - return when { - iconCustom.isUnknown -> super.icon - else -> iconCustom - } - } - set(value) { - if (value is IconImageStandard) - iconCustom = IconImageCustom.UNKNOWN_ICON - super.icon = value - } - var iconCustom = IconImageCustom.UNKNOWN_ICON var customData = LinkedHashMap() var fields = LinkedHashMap() var binaries = LinkedHashMap() // Map @@ -72,7 +56,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte var additional = "" var tags = "" - fun getSize(binaryPool: BinaryPool): Long { + fun getSize(attachmentPool: AttachmentPool): Long { var size = FIXED_LENGTH_SIZE for (entry in fields.entries) { @@ -80,7 +64,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte size += entry.value.length().toLong() } - size += getAttachmentsSize(binaryPool) + size += getAttachmentsSize(attachmentPool) size += autoType.defaultSequence.length.toLong() for ((key, value) in autoType.entrySet()) { @@ -89,7 +73,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte } for (entry in history) { - size += entry.getSize(binaryPool) + size += entry.getSize(attachmentPool) } size += overrideURL.length.toLong() @@ -103,7 +87,6 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte constructor() : super() constructor(parcel: Parcel) : super(parcel) { - iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom usageCount = UnsignedLong(parcel.readLong()) locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged customData = ParcelableUtil.readStringParcelableMap(parcel) @@ -121,7 +104,6 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte override fun writeToParcel(dest: Parcel, flags: Int) { super.writeToParcel(dest, flags) - dest.writeParcelable(iconCustom, flags) dest.writeLong(usageCount.toKotlinLong()) dest.writeParcelable(locationChanged, flags) ParcelableUtil.writeStringParcelableMap(dest, customData) @@ -143,7 +125,6 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte */ fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) { super.updateWith(source) - iconCustom = IconImageCustom(source.iconCustom) usageCount = source.usageCount locationChanged = DateInstant(source.locationChanged) // Add all custom elements in map @@ -281,16 +262,16 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte /** * It's a list because history labels can be defined multiple times */ - fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List { + fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List { val entryAttachmentList = ArrayList() for ((label, poolId) in binaries) { - binaryPool[poolId]?.let { binary -> + attachmentPool[poolId]?.let { binary -> entryAttachmentList.add(Attachment(label, binary)) } } if (inHistory) { history.forEach { - entryAttachmentList.addAll(it.getAttachments(binaryPool, false)) + entryAttachmentList.addAll(it.getAttachments(attachmentPool, false)) } } return entryAttachmentList @@ -300,8 +281,8 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte return binaries.isNotEmpty() } - fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { - binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment) + fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) { + binaries[attachment.name] = attachmentPool.put(attachment.binaryData) } fun removeAttachment(attachment: Attachment) { @@ -312,11 +293,11 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte binaries.clear() } - private fun getAttachmentsSize(binaryPool: BinaryPool): Long { + private fun getAttachmentsSize(attachmentPool: AttachmentPool): Long { var size = 0L for ((label, poolId) in binaries) { size += label.length.toLong() - size += binaryPool[poolId]?.length ?: 0 + size += attachmentPool[poolId]?.getSize() ?: 0 } return size } @@ -333,7 +314,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte history.add(entry) } - fun removeEntryFromHistory(position: Int): EntryKDBX? { + fun removeEntryFromHistory(position: Int): EntryKDBX { return history.removeAt(position) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt index 2a44ab229..a220c1655 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt @@ -82,10 +82,6 @@ class GroupKDB : GroupVersioned, NodeKDBInterface this.nodeId = NodeIdInt(groupId) } - override fun allowAddEntryIfIsRoot(): Boolean { - return false - } - companion object { @JvmField diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt index 8f2d99903..450524a04 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt @@ -21,37 +21,18 @@ package com.kunzisoft.keepass.database.element.group import android.os.Parcel import android.os.Parcelable -import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.entry.EntryKDBX -import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.database.element.icon.IconImageCustom -import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.utils.UnsignedLong - -import java.util.HashMap -import java.util.UUID +import java.util.* class GroupKDBX : GroupVersioned, NodeKDBXInterface { - // TODO Encapsulate - override var icon: IconImage - get() { - return if (iconCustom.isUnknown) - super.icon - else - iconCustom - } - set(value) { - if (value is IconImageStandard) - iconCustom = IconImageCustom.UNKNOWN_ICON - super.icon = value - } - var iconCustom = IconImageCustom.UNKNOWN_ICON private val customData = HashMap() var notes = "" @@ -77,7 +58,6 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte constructor() : super() constructor(parcel: Parcel) : super(parcel) { - iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom usageCount = UnsignedLong(parcel.readLong()) locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged // TODO customData = ParcelableUtil.readStringParcelableMap(parcel); @@ -101,7 +81,6 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte override fun writeToParcel(dest: Parcel, flags: Int) { super.writeToParcel(dest, flags) - dest.writeParcelable(iconCustom, flags) dest.writeLong(usageCount.toKotlinLong()) dest.writeParcelable(locationChanged, flags) // TODO ParcelableUtil.writeStringParcelableMap(dest, customData); @@ -115,7 +94,6 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte fun updateWith(source: GroupKDBX) { super.updateWith(source) - iconCustom = IconImageCustom(source.iconCustom) usageCount = source.usageCount locationChanged = DateInstant(source.locationChanged) // Add all custom elements in map @@ -147,10 +125,6 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte return customData.isNotEmpty() } - override fun allowAddEntryIfIsRoot(): Boolean { - return true - } - companion object { @JvmField diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersionedInterface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersionedInterface.kt index 7e3db5f09..f4450dbf5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersionedInterface.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersionedInterface.kt @@ -38,8 +38,6 @@ interface GroupVersionedInterface, fun removeChildren() - fun allowAddEntryIfIsRoot(): Boolean - @Suppress("UNCHECKED_CAST") fun doForEachChildAndForIt(entryHandler: NodeHandler, groupHandler: NodeHandler) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImage.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImage.kt index 8f8ccf6f3..17d59cb1f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImage.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Jeremy Jamet / Kunzisoft. + * Copyright 2021 Jeremy Jamet / Kunzisoft. * * This file is part of KeePassDX. * @@ -19,19 +19,69 @@ */ package com.kunzisoft.keepass.database.element.icon +import android.os.Parcel import android.os.Parcelable -abstract class IconImage protected constructor() : Parcelable { +class IconImage() : IconImageDraw(), Parcelable { - abstract val iconId: Int - abstract val isUnknown: Boolean - abstract val isMetaStreamIcon: Boolean + var standard: IconImageStandard = IconImageStandard() + var custom: IconImageCustom = IconImageCustom() + + constructor(iconImageStandard: IconImageStandard) : this() { + this.standard = iconImageStandard + } + + constructor(iconImageCustom: IconImageCustom) : this() { + this.custom = iconImageCustom + } + + constructor(iconImageStandard: IconImageStandard, + iconImageCustom: IconImageCustom) : this() { + this.standard = iconImageStandard + this.custom = iconImageCustom + } + + constructor(parcel: Parcel) : this() { + standard = parcel.readParcelable(IconImageStandard::class.java.classLoader) ?: standard + custom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: custom + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(standard, flags) + parcel.writeParcelable(custom, flags) + } override fun describeContents(): Int { return 0 } - companion object { - const val UNKNOWN_ID = -1 + override fun getIconImageToDraw(): IconImage { + return this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IconImage) return false + + if (standard != other.standard) return false + if (custom != other.custom) return false + + return true + } + + override fun hashCode(): Int { + var result = standard.hashCode() + result = 31 * result + custom.hashCode() + return result + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): IconImage { + return IconImage(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt index 35069831d..efc039ee4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Brian Pellin, Jeremy Jamet / Kunzisoft. + * Copyright 2021 Jeremy Jamet / Kunzisoft. * * This file is part of KeePassDX. * @@ -22,39 +22,30 @@ package com.kunzisoft.keepass.database.element.icon import android.os.Parcel import android.os.Parcelable import com.kunzisoft.keepass.database.element.database.DatabaseVersioned +import java.util.* -import java.util.UUID +class IconImageCustom : Parcelable, IconImageDraw { -class IconImageCustom : IconImage { + var uuid: UUID - val uuid: UUID - @Transient - var imageData: ByteArray = ByteArray(0) - - constructor(uuid: UUID, data: ByteArray) : super() { - this.uuid = uuid - this.imageData = data + constructor() { + uuid = DatabaseVersioned.UUID_ZERO } - constructor(uuid: UUID) : super() { + constructor(uuid: UUID) { this.uuid = uuid - this.imageData = ByteArray(0) - } - - constructor(icon: IconImageCustom) : super() { - uuid = icon.uuid - imageData = icon.imageData } constructor(parcel: Parcel) { uuid = parcel.readSerializable() as UUID - // TODO Take too much memories - // parcel.readByteArray(imageData); + } + + override fun describeContents(): Int { + return 0 } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(uuid) - // Too big for a parcelable dest.writeByteArray(imageData); } override fun hashCode(): Int { @@ -64,6 +55,10 @@ class IconImageCustom : IconImage { return result } + override fun getIconImageToDraw(): IconImage { + return IconImage(this) + } + override fun equals(other: Any?): Boolean { if (this === other) return true @@ -74,17 +69,10 @@ class IconImageCustom : IconImage { return uuid == other.uuid } - override val iconId: Int - get() = UNKNOWN_ID - - override val isUnknown: Boolean - get() = this == UNKNOWN_ICON - - override val isMetaStreamIcon: Boolean - get() = false + val isUnknown: Boolean + get() = uuid == DatabaseVersioned.UUID_ZERO companion object { - val UNKNOWN_ICON = IconImageCustom(DatabaseVersioned.UUID_ZERO, ByteArray(0)) @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageDraw.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageDraw.kt new file mode 100644 index 000000000..978999f39 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageDraw.kt @@ -0,0 +1,29 @@ +/* + * 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.database.element.icon + +abstract class IconImageDraw { + + var selected = false + /** + * Only to retrieve an icon image to Draw, to not use as object to manipulate + */ + abstract fun getIconImageToDraw(): IconImage +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageFactory.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageFactory.kt deleted file mode 100644 index f48a8b264..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageFactory.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2019 Brian Pellin, 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.database.element.icon - -import org.apache.commons.collections.map.AbstractReferenceMap -import org.apache.commons.collections.map.ReferenceMap - -import java.util.UUID - -class IconImageFactory { - /** customIconMap - * Cache for icon drawable. - * Keys: Integer, Values: IconImageStandard - */ - private val cache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK) - - /** standardIconMap - * Cache for icon drawable. - * Keys: UUID, Values: IconImageCustom - */ - private val customCache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK) - - val unknownIcon: IconImageStandard - get() = getIcon(IconImage.UNKNOWN_ID) - - val keyIcon: IconImageStandard - get() = getIcon(IconImageStandard.KEY) - - val trashIcon: IconImageStandard - get() = getIcon(IconImageStandard.TRASH) - - val folderIcon: IconImageStandard - get() = getIcon(IconImageStandard.FOLDER) - - fun getIcon(iconId: Int): IconImageStandard { - var icon: IconImageStandard? = cache[iconId] as IconImageStandard? - - if (icon == null) { - icon = IconImageStandard(iconId) - cache[iconId] = icon - } - - return icon - } - - fun getIcon(iconUuid: UUID): IconImageCustom { - var icon: IconImageCustom? = customCache[iconUuid] as IconImageCustom? - - if (icon == null) { - icon = IconImageCustom(iconUuid) - customCache[iconUuid] = icon - } - - return icon - } - - fun put(icon: IconImageCustom) { - customCache[icon.uuid] = icon - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageStandard.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageStandard.kt index bd20c38a3..bc5ea8543 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageStandard.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageStandard.kt @@ -21,36 +21,46 @@ package com.kunzisoft.keepass.database.element.icon import android.os.Parcel import android.os.Parcelable +import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS -class IconImageStandard : IconImage { +class IconImageStandard : Parcelable, IconImageDraw { + + val id: Int constructor() { - this.iconId = KEY + this.id = KEY_ID } constructor(iconId: Int) { - this.iconId = iconId + if (!isCorrectIconId(iconId)) + this.id = KEY_ID + else + this.id = iconId } - constructor(icon: IconImageStandard) { - this.iconId = icon.iconId + constructor(parcel: Parcel) { + id = parcel.readInt() } - constructor(parcel: Parcel) { - iconId = parcel.readInt() + override fun describeContents(): Int { + return 0 } override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeInt(iconId) + dest.writeInt(id) } override fun hashCode(): Int { val prime = 31 var result = 1 - result = prime * result + iconId + result = prime * result + id return result } + override fun getIconImageToDraw(): IconImage { + return IconImage(this) + } + override fun equals(other: Any?): Boolean { if (this === other) return true @@ -59,22 +69,18 @@ class IconImageStandard : IconImage { if (other !is IconImageStandard) { return false } - return iconId == other.iconId + return id == other.id } - override val iconId: Int - - override val isUnknown: Boolean - get() = iconId == UNKNOWN_ID - - override val isMetaStreamIcon: Boolean - get() = iconId == 0 - companion object { - const val KEY = 0 - const val TRASH = 43 - const val FOLDER = 48 + const val KEY_ID = 0 + const val TRASH_ID = 43 + const val FOLDER_ID = 48 + + fun isCorrectIconId(iconId: Int): Boolean { + return iconId in 0 until NB_ICONS + } @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt new file mode 100644 index 000000000..d4b9a06e9 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt @@ -0,0 +1,120 @@ +/* + * 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.database.element.icon + +import android.util.Log +import com.kunzisoft.keepass.database.element.database.BinaryByte +import com.kunzisoft.keepass.database.element.database.BinaryByte.Companion.MAX_BINARY_BYTES +import com.kunzisoft.keepass.database.element.database.BinaryData +import com.kunzisoft.keepass.database.element.database.BinaryFile +import com.kunzisoft.keepass.database.element.database.CustomIconPool +import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID +import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS +import java.io.File +import java.util.* + +class IconsManager { + + private val standardCache = List(NB_ICONS) { + IconImageStandard(it) + } + private val customCache = CustomIconPool() + + fun getIcon(iconId: Int): IconImageStandard { + val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID + return standardCache[searchIconId] + } + + fun doForEachStandardIcon(action: (IconImageStandard) -> Unit) { + standardCache.forEach { icon -> + action.invoke(icon) + } + } + + /* + * Custom + */ + + fun buildNewCustomIcon(cacheDirectory: File, + key: UUID? = null, + result: (IconImageCustom, BinaryData?) -> Unit) { + // Create a binary file for a brand new custom icon + addCustomIcon(cacheDirectory, key, -1, result) + } + + fun addCustomIcon(cacheDirectory: File, + key: UUID? = null, + dataSize: Int, + result: (IconImageCustom, BinaryData?) -> Unit) { + val keyBinary = customCache.put(key) { uniqueBinaryId -> + // Create a byte array for better performance with small data + if (dataSize in 1..MAX_BINARY_BYTES) { + BinaryByte() + } else { + val fileInCache = File(cacheDirectory, uniqueBinaryId) + BinaryFile(fileInCache) + } + } + result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary) + } + + fun getIcon(iconUuid: UUID): IconImageCustom { + return IconImageCustom(iconUuid) + } + + fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { + return customCache.isBinaryDuplicate(binaryData) + } + + fun removeCustomIcon(iconUuid: UUID) { + val binary = customCache[iconUuid] + customCache.remove(iconUuid) + try { + binary?.clear() + } catch (e: Exception) { + Log.w(TAG, "Unable to remove custom icon binary", e) + } + } + + fun getBinaryForCustomIcon(iconUuid: UUID): BinaryData? { + return customCache[iconUuid] + } + + fun doForEachCustomIcon(action: (IconImageCustom, BinaryData) -> Unit) { + customCache.doForEachBinary { key, binary -> + action.invoke(IconImageCustom(key), binary) + } + } + + /** + * Clear the cache of icons + */ + fun clearCache() { + try { + customCache.clear() + } catch(e: Exception) { + Log.e(TAG, "Unable to clear cache", e) + } + } + + companion object { + private val TAG = IconsManager::class.java.name + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt index e29e4a08a..1c87f49b3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt @@ -22,11 +22,10 @@ package com.kunzisoft.keepass.database.element.node import android.os.Parcel import android.os.Parcelable -import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.database.element.icon.IconImageStandard import org.joda.time.LocalDateTime /** @@ -88,7 +87,7 @@ abstract class NodeVersioned { @@ -260,7 +261,7 @@ class DatabaseInputKDB(cacheDirectory: File) } 0x0007 -> { newGroup?.let { group -> - group.icon = mDatabase.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) + group.icon.standard = mDatabase.getStandardIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt()) } ?: newEntry?.let { entry -> entry.password = cipherInputStream.readBytesToString(fieldSize,false) @@ -305,7 +306,7 @@ class DatabaseInputKDB(cacheDirectory: File) 0x000E -> { newEntry?.let { entry -> if (fieldSize > 0) { - val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory) + val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory) entry.binaryData = binaryAttachment val cipherKey = mDatabase.loadedCipherKey ?: throw IOException("Unable to retrieve cipher key to load binaries") 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 185af9ba6..e446ba5a0 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 @@ -29,14 +29,13 @@ 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 +import com.kunzisoft.keepass.database.element.database.BinaryData 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.DatabaseVersioned import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX -import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.security.ProtectedString @@ -79,7 +78,7 @@ class DatabaseInputKDBX(cacheDirectory: File) private var ctxStringName: String? = null private var ctxStringValue: ProtectedString? = null private var ctxBinaryName: String? = null - private var ctxBinaryValue: BinaryAttachment? = null + private var ctxBinaryValue: BinaryData? = null private var ctxATName: String? = null private var ctxATSeq: String? = null private var entryInHistory = false @@ -277,7 +276,7 @@ class DatabaseInputKDBX(cacheDirectory: File) val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected val byteLength = size - 1 // No compression at this level - val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag) + val protectedBinary = mDatabase.buildNewAttachment(cacheDirectory, false, protectedFlag) val cipherKey = mDatabase.loadedCipherKey ?: throw IOException("Unable to retrieve cipher key to load binaries") protectedBinary.getOutputDataStream(cipherKey).use { outputStream -> @@ -507,9 +506,9 @@ class DatabaseInputKDBX(cacheDirectory: File) } else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) { ctxGroup?.notes = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { - ctxGroup?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) + ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { - ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp)) + ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp)) } else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) { return switchContext(ctx, KdbContext.GroupTimes, xpp) } else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) { @@ -561,9 +560,9 @@ class DatabaseInputKDBX(cacheDirectory: File) KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) { ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp)) } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { - ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) + ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { - ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp)) + ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp)) } else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) { ctxEntry?.foregroundColor = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) { @@ -705,9 +704,13 @@ class DatabaseInputKDBX(cacheDirectory: File) return KdbContext.Meta } else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) { if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) { - val icon = IconImageCustom(customIconID, customIconData!!) - mDatabase.addCustomIcon(icon) - mDatabase.iconFactory.put(icon) + mDatabase.addCustomIcon(cacheDirectory, customIconID, customIconData!!.size) { _, binary -> + mDatabase.loadedCipherKey?.let { cipherKey -> + binary?.getOutputDataStream(cipherKey)?.use { outputStream -> + outputStream.write(customIconData) + } + } + } } customIconID = DatabaseVersioned.UUID_ZERO @@ -963,7 +966,7 @@ class DatabaseInputKDBX(cacheDirectory: File) } @Throws(XmlPullParserException::class, IOException::class) - private fun readBinary(xpp: XmlPullParser): BinaryAttachment? { + private fun readBinary(xpp: XmlPullParser): BinaryData? { // Reference Id to a binary already present in binary pool val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef) @@ -978,7 +981,7 @@ class DatabaseInputKDBX(cacheDirectory: File) var binaryRetrieve = mDatabase.binaryPool[id] // Create empty binary if not retrieved in pool if (binaryRetrieve == null) { - binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory, + binaryRetrieve = mDatabase.buildNewAttachment(cacheDirectory, compression = false, protection = false, binaryPoolId = id) } return binaryRetrieve @@ -994,7 +997,7 @@ class DatabaseInputKDBX(cacheDirectory: File) } @Throws(IOException::class, XmlPullParserException::class) - private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? { + private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryData? { var compressed = false var protected = true @@ -1015,7 +1018,7 @@ class DatabaseInputKDBX(cacheDirectory: File) return null // Build the new binary and compress - val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId) + val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory, compressed, protected, binaryId) val binaryCipherKey = mDatabase.loadedCipherKey ?: throw IOException("Unable to retrieve cipher key to load binaries") try { 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 6c145529f..a59752eb4 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 @@ -216,10 +216,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, GroupOutputKDB(group, outputStream).output() } // Entries - val binaryCipherKey = mDatabaseKDB.loadedCipherKey - ?: throw DatabaseOutputException("Unable to retrieve cipher key to write binaries") mDatabaseKDB.doForEachEntryInIndex { entry -> - EntryOutputKDB(entry, outputStream, binaryCipherKey).output() + EntryOutputKDB(entry, outputStream, mDatabaseKDB.loadedCipherKey).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 27a98c388..77e5b9ccf 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 @@ -35,7 +35,6 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BA import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX -import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.ProtectedString @@ -137,29 +136,28 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, dataOutputStream.writeInt(streamKeySize) dataOutputStream.write(header.innerRandomStreamKey) - 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(binaryCipherKey) - // Write type binary - dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) - // Write size - dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length + 1)) - // Write protected flag - var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None - if (protectedBinary.isProtected) { - flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected - } - dataOutputStream.writeByte(flag) + database.loadedCipherKey?.let { binaryCipherKey -> + database.binaryPool.doForEachOrderedBinaryWithoutDuplication { _, binary -> + // Force decompression to add binary in header + binary.decompress(binaryCipherKey) + // Write type binary + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) + // Write size + dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1)) + // Write protected flag + var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None + if (binary.isProtected) { + flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected + } + dataOutputStream.writeByte(flag) - protectedBinary.getInputDataStream(binaryCipherKey).use { inputStream -> - inputStream.readAllBytes { buffer -> - dataOutputStream.write(buffer) + binary.getInputDataStream(binaryCipherKey).use { inputStream -> + inputStream.readAllBytes { buffer -> + dataOutputStream.write(buffer) + } } } - } + } ?: Log.e(TAG, "Unable to retrieve cipher key to write head binaries") dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader) dataOutputStream.writeInt(0) @@ -363,10 +361,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, writeUuid(DatabaseKDBXXML.ElemUuid, group.id) writeObject(DatabaseKDBXXML.ElemName, group.title) writeObject(DatabaseKDBXXML.ElemNotes, group.notes) - writeObject(DatabaseKDBXXML.ElemIcon, group.icon.iconId.toLong()) + writeObject(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong()) - if (group.iconCustom != IconImageCustom.UNKNOWN_ICON) { - writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.iconCustom.uuid) + if (!group.icon.custom.isUnknown) { + writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid) } writeTimes(group) @@ -388,10 +386,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, xml.startTag(null, DatabaseKDBXXML.ElemEntry) writeUuid(DatabaseKDBXXML.ElemUuid, entry.id) - writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.iconId.toLong()) + writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong()) - if (entry.iconCustom != IconImageCustom.UNKNOWN_ICON) { - writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.iconCustom.uuid) + if (!entry.icon.custom.isUnknown) { + writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid) } writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor) @@ -496,30 +494,31 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, // With kdbx4, don't use this method because binaries are in header file @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeMetaBinaries() { - xml.startTag(null, DatabaseKDBXXML.ElemBinaries) - - // Use indexes because necessarily (binary header ref is the order) - mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary -> - xml.startTag(null, DatabaseKDBXXML.ElemBinary) - xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) - val binary = keyBinary.binary - if (binary.length > 0) { - if (binary.isCompressed) { - xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) - } - // Write the XML - 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))) + mDatabaseKDBX.loadedCipherKey?.let { binaryCipherKey -> + xml.startTag(null, DatabaseKDBXXML.ElemBinaries) + // Use indexes because necessarily (binary header ref is the order) + mDatabaseKDBX.binaryPool.doForEachOrderedBinaryWithoutDuplication { index, binary -> + xml.startTag(null, DatabaseKDBXXML.ElemBinary) + xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) + if (binary.getSize() > 0) { + if (binary.isCompressed) { + xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) + } + try { + // Write the XML + binary.getInputDataStream(binaryCipherKey).use { inputStream -> + inputStream.readAllBytes { buffer -> + xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to write binary", e) } } + xml.endTag(null, DatabaseKDBXXML.ElemBinary) } - xml.endTag(null, DatabaseKDBXXML.ElemBinary) - } - - xml.endTag(null, DatabaseKDBXXML.ElemBinaries) + xml.endTag(null, DatabaseKDBXXML.ElemBinaries) + } ?: Log.e(TAG, "Unable to retrieve cipher key to write binaries") } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @@ -700,21 +699,39 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeCustomIconList() { - val customIcons = mDatabaseKDBX.customIcons - if (customIcons.size == 0) return - - xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons) - - for (icon in customIcons) { - xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem) - - writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, icon.uuid) - writeObject(DatabaseKDBXXML.ElemCustomIconItemData, String(Base64.encode(icon.imageData, BASE_64_FLAG))) + mDatabaseKDBX.loadedCipherKey?.let { cipherKey -> + var firstElement = true + mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary -> + if (binary.dataExists()) { + // Write the parent tag + if (firstElement) { + xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons) + firstElement = false + } - xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem) - } + xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem) + + writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid) + var customImageData = ByteArray(0) + try { + binary.getInputDataStream(cipherKey).use { inputStream -> + customImageData = inputStream.readBytes() + } + } catch (e: Exception) { + Log.e(TAG, "Unable to write custom icon", e) + } finally { + writeObject(DatabaseKDBXXML.ElemCustomIconItemData, + String(Base64.encode(customImageData, BASE_64_FLAG))) + } - xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons) + xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem) + } + } + // Close the parent tag + if (!firstElement) { + xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons) + } + } ?: Log.e(TAG, "Unable to retrieve cipher key to write custom icons") } private fun safeXmlString(text: String): String { 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 25461e985..a18aa8201 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,6 +19,7 @@ */ package com.kunzisoft.keepass.database.file.output +import android.util.Log import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.exception.DatabaseOutputException @@ -34,7 +35,7 @@ import java.nio.charset.Charset */ class EntryOutputKDB(private val mEntry: EntryKDB, private val mOutputStream: OutputStream, - private val mCipherKey: Database.LoadedKey) { + private val mCipherKey: Database.LoadedKey?) { //NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int @Throws(DatabaseOutputException::class) @@ -53,7 +54,7 @@ class EntryOutputKDB(private val mEntry: EntryKDB, // Image ID mOutputStream.write(IMAGEID_FIELD_TYPE) mOutputStream.write(IMAGEID_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId))) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.standard.id))) // Title //byte[] title = mEntry.title.getBytes("UTF-8"); @@ -93,20 +94,22 @@ class EntryOutputKDB(private val mEntry: EntryKDB, StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription) // 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) + mCipherKey?.let { cipherKey -> + mOutputStream.write(BINARY_DATA_FIELD_TYPE) + val binaryData = mEntry.binaryData + val binaryDataLength = binaryData?.getSize() ?: 0L + // Write data length + mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength))) + // Write data + if (binaryDataLength > 0) { + binaryData?.getInputDataStream(cipherKey).use { inputStream -> + inputStream?.readAllBytes { buffer -> + mOutputStream.write(buffer) + } + inputStream?.close() } - inputStream?.close() } - } + } ?: Log.e(TAG, "Unable to retrieve cipher key to write entry binary") // End mOutputStream.write(END_FIELD_TYPE) @@ -138,6 +141,8 @@ class EntryOutputKDB(private val mEntry: EntryKDB, } companion object { + + private val TAG = EntryOutputKDB::class.java.name // Constants private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1) private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2) 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 326dcf543..2eadf7ec3 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 @@ -72,7 +72,7 @@ class GroupOutputKDB(private val mGroup: GroupKDB, // Image ID mOutputStream.write(IMAGEID_FIELD_TYPE) mOutputStream.write(IMAGEID_FIELD_SIZE) - mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId))) + mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.standard.id))) // Level mOutputStream.write(LEVEL_FIELD_TYPE) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt index 015ba2c60..481aae69d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt @@ -81,6 +81,7 @@ class SearchHelper { max: Int): Group? { val searchGroup = database.createGroup() + searchGroup?.isVirtual = true searchGroup?.title = "\"" + searchQuery + "\"" // Search all entries diff --git a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt index eb8b47ad1..8b7d52e71 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.education import android.app.Activity import android.content.Context import android.content.SharedPreferences +import android.graphics.Color import android.util.Log import androidx.preference.PreferenceManager import com.getkeepsafe.taptargetview.TapTarget @@ -74,6 +75,24 @@ open class Education(val activity: Activity) { editor.apply() } + protected fun getCircleColor(): Int { + val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationCircleColor)) + val colorControl = typedArray.getColor(0, Color.GREEN) + typedArray.recycle() + return colorControl + } + + protected fun getCircleAlpha(): Float { + return 0.98F + } + + protected fun getTextColor(): Int { + val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationTextColor)) + val colorControl = typedArray.getColor(0, Color.WHITE) + typedArray.recycle() + return colorControl + } + companion object { private const val EDUCATION_PREFERENCE = "kdbxeducation" diff --git a/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt index 728f1967d..5f45d85f7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.education import android.app.Activity -import android.graphics.Color import android.view.View import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView @@ -38,7 +37,9 @@ class EntryActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_field_copy_title), activity.getString(R.string.education_field_copy_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(false) .cancelable(true), object : TapTargetView.Listener() { @@ -68,7 +69,9 @@ class EntryActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_entry_edit_title), activity.getString(R.string.education_entry_edit_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { diff --git a/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt index f9eb011b5..b4aa83ba5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.education import android.app.Activity -import android.graphics.Color import android.view.View import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView @@ -40,7 +39,9 @@ class EntryEditActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_generate_password_title), activity.getString(R.string.education_generate_password_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -69,7 +70,9 @@ class EntryEditActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_entry_new_field_title), activity.getString(R.string.education_entry_new_field_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -98,7 +101,9 @@ class EntryEditActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_add_attachment_title), activity.getString(R.string.education_add_attachment_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -127,7 +132,9 @@ class EntryEditActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_setup_OTP_title), activity.getString(R.string.education_setup_OTP_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { diff --git a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt index 686a74d17..7a58b943c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt @@ -21,8 +21,8 @@ package com.kunzisoft.keepass.education import android.app.Activity import android.graphics.Color -import androidx.core.content.ContextCompat import android.view.View +import androidx.core.content.ContextCompat import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView import com.kunzisoft.keepass.R @@ -43,8 +43,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_create_database_title), activity.getString(R.string.education_create_database_summary)) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp)) - .textColorInt(Color.WHITE) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -73,8 +75,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_select_database_title), activity.getString(R.string.education_select_database_summary)) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp)) - .textColorInt(Color.WHITE) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { diff --git a/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt index cb7d414e6..a083f31a0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.education import android.app.Activity -import android.graphics.Color import android.view.View import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView @@ -36,7 +35,9 @@ class GroupActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_new_node_title), activity.getString(R.string.education_new_node_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(false) .cancelable(true), object : TapTargetView.Listener() { @@ -61,7 +62,9 @@ class GroupActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_search_title), activity.getString(R.string.education_search_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -86,7 +89,9 @@ class GroupActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_sort_title), activity.getString(R.string.education_sort_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -111,7 +116,9 @@ class GroupActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_lock_title), activity.getString(R.string.education_lock_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { diff --git a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt index d3d3a044e..8eed1496b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.education import android.app.Activity -import android.graphics.Color import androidx.core.content.ContextCompat import android.view.View import com.getkeepsafe.taptargetview.TapTarget @@ -37,8 +36,10 @@ class PasswordActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_unlock_title), activity.getString(R.string.education_unlock_summary)) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) .icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round)) - .textColorInt(Color.WHITE) + .textColorInt(getTextColor()) .tintTarget(false) .cancelable(true), object : TapTargetView.Listener() { @@ -63,7 +64,9 @@ class PasswordActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_read_only_title), activity.getString(R.string.education_read_only_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(true) .cancelable(true), object : TapTargetView.Listener() { @@ -88,7 +91,9 @@ class PasswordActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_advanced_unlock_title), activity.getString(R.string.education_advanced_unlock_summary)) - .textColorInt(Color.WHITE) + .outerCircleColorInt(getCircleColor()) + .outerCircleAlpha(getCircleAlpha()) + .textColorInt(getTextColor()) .tintTarget(false) .cancelable(true), object : TapTargetView.Listener() { diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt index add39ee52..038bdfe5b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt @@ -26,171 +26,100 @@ import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable -import android.graphics.drawable.Icon -import android.os.Build import android.util.Log import android.widget.ImageView import android.widget.RemoteViews -import androidx.annotation.RequiresApi import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.widget.ImageViewCompat import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.icon.IconImage +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.database.BinaryData import com.kunzisoft.keepass.database.element.icon.IconImageCustom -import com.kunzisoft.keepass.database.element.icon.IconImageStandard -import org.apache.commons.collections.map.AbstractReferenceMap -import org.apache.commons.collections.map.ReferenceMap +import com.kunzisoft.keepass.database.element.icon.IconImageDraw +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.ref.WeakReference +import java.util.* +import kotlin.collections.HashMap /** * Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint */ -class IconDrawableFactory { +class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedKey?, + private val retrieveCustomIconBinary : (iconId: UUID) -> BinaryData?) { /** customIconMap * Cache for icon drawable. * Keys: UUID, Values: Drawables */ - private val customIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK) + private val customIconMap = HashMap>() /** standardIconMap * Cache for icon drawable. * Keys: Integer, Values: Drawables */ - private val standardIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK) + private val standardIconMap = HashMap>() /** - * Utility method to assign a drawable to an ImageView and tint it + * Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed */ - fun assignDrawableToImageView(superDrawable: SuperDrawable, imageView: ImageView?, tint: Boolean, tintColor: Int) { - if (imageView != null) { - imageView.setImageDrawable(superDrawable.drawable) - if (superDrawable.tintable && tint) { - ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor)) - } else { - ImageViewCompat.setImageTintList(imageView, null) + private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable { + val icon = iconDraw.getIconImageToDraw() + val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid) + if (customIconBinary != null && customIconBinary.dataExists()) { + getIconDrawable(context.resources, icon.custom, customIconBinary)?.let { + return SuperDrawable(it) } } - } - - /** - * Utility method to assign a drawable to a RemoteView and tint it - */ - fun assignDrawableToRemoteViews(superDrawable: SuperDrawable, - remoteViews: RemoteViews, - imageId: Int, - tintColor: Int = Color.BLACK) { - val bitmap = superDrawable.drawable.toBitmap() - // Tint bitmap if it's not a custom icon - if (superDrawable.tintable && bitmap.isMutable) { - Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply { - colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN) - }) - } - remoteViews.setImageViewBitmap(imageId, bitmap) - } - - /** - * Utility method to assign a drawable to a icon and tint it - */ - @RequiresApi(Build.VERSION_CODES.M) - fun assignDrawableToIcon(superDrawable: SuperDrawable, - tintColor: Int = Color.BLACK): Icon { - val bitmap = superDrawable.drawable.toBitmap() - // Tint bitmap if it's not a custom icon - if (superDrawable.tintable && bitmap.isMutable) { - Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply { - colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN) - }) - } - return Icon.createWithBitmap(bitmap) - } - - /** - * Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed - */ - fun getIconSuperDrawable(context: Context, icon: IconImage, width: Int, tint: Boolean = false, tintColor: Int = Color.WHITE): SuperDrawable { - return when (icon) { - is IconImageStandard -> { - val resId = IconPackChooser.getSelectedIconPack(context)?.iconToResId(icon.iconId) ?: R.drawable.ic_blank_32dp - getIconSuperDrawable(context, resId, width, tint, tintColor) - } - is IconImageCustom -> { - SuperDrawable(getIconDrawable(context.resources, icon)) - } - else -> { - SuperDrawable(PatternIcon(context.resources).blankDrawable) - } - } - } - - /** - * Get the [SuperDrawable] IconImageStandard from [iconId] (cache, or build it and add it to the cache if not exists yet) - * , then [tint] it with [tintColor] if needed - */ - fun getIconSuperDrawable(context: Context, iconId: Int, width: Int, tint: Boolean, tintColor: Int): SuperDrawable { - return SuperDrawable(getIconDrawable(context.resources, iconId, width, tint, tintColor), true) - } - - /** - * Key class to retrieve a Drawable in the cache if it's tinted or not - */ - private inner class CacheKey(var resId: Int, var density: Int, var isTint: Boolean, var color: Int) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || javaClass != other.javaClass) return false - val cacheKey = other as CacheKey - return if (isTint) - resId == cacheKey.resId - && density == cacheKey.density - && cacheKey.isTint - && color == cacheKey.color - else - resId == cacheKey.resId - && density == cacheKey.density - && !cacheKey.isTint - } - - override fun hashCode(): Int { - var result = resId - result = 31 * result + density - result = 31 * result + isTint.hashCode() - result = 31 * result + color - return result + val iconPack = IconPackChooser.getSelectedIconPack(context) + iconPack?.iconToResId(icon.standard.id)?.let { iconId -> + return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable()) + } ?: run { + return SuperDrawable(PatternIcon(context.resources).blankDrawable) } } /** * Build a custom [Drawable] from custom [icon] */ - private fun getIconDrawable(resources: Resources, icon: IconImageCustom): Drawable { + private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? { val patternIcon = PatternIcon(resources) - - var draw: Drawable? = customIconMap[icon.uuid] as Drawable? - if (draw == null) { - var bitmap: Bitmap? = BitmapFactory.decodeByteArray(icon.imageData, 0, icon.imageData.size) - // Could not understand custom icon - bitmap?.let { bitmapIcon -> - bitmap = resize(bitmapIcon, patternIcon) - draw = BitmapDrawable(resources, bitmap) - customIconMap[icon.uuid] = draw - return draw!! + val cipherKey = retrieveCipherKey() + if (cipherKey != null) { + val draw: Drawable? = customIconMap[icon.uuid]?.get() + if (draw == null) { + iconCustomBinary?.let { binaryFile -> + try { + var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(cipherKey)) + bitmap?.let { bitmapIcon -> + bitmap = resize(bitmapIcon, patternIcon) + val createdDraw = BitmapDrawable(resources, bitmap) + customIconMap[icon.uuid] = WeakReference(createdDraw) + return createdDraw + } + } catch (e: Exception) { + customIconMap.remove(icon.uuid) + Log.e(TAG, "Unable to create the bitmap icon", e) + } + } + } else { + return draw } - } else { - return draw!! } - return patternIcon.blankDrawable + return null } /** * Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet) - * , then [tint] it with [tintColor] if needed + * , then tint it with [tintColor] if needed */ - private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tint: Boolean, tintColor: Int): Drawable { - val newCacheKey = CacheKey(iconId, width, tint, tintColor) + private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tintColor: Int): Drawable { + val newCacheKey = CacheKey(iconId, width, true, tintColor) - var draw: Drawable? = standardIconMap[newCacheKey] as Drawable? + var draw: Drawable? = standardIconMap[newCacheKey]?.get() if (draw == null) { try { draw = ResourcesCompat.getDrawable(resources, iconId, null) @@ -199,7 +128,7 @@ class IconDrawableFactory { } if (draw != null) { - standardIconMap[newCacheKey] = draw + standardIconMap[newCacheKey] = WeakReference(draw) } } @@ -227,6 +156,78 @@ class IconDrawableFactory { } + /** + * Assign a database [icon] to an ImageView and tint it with [tintColor] if needed + */ + fun assignDatabaseIcon(imageView: ImageView, + icon: IconImageDraw, + tintColor: Int = Color.WHITE) { + try { + val context = imageView.context + CoroutineScope(Dispatchers.IO).launch { + addToCustomCache(context.resources, icon) + withContext(Dispatchers.Main) { + val superDrawable = getIconSuperDrawable(context, + icon, + imageView.width, + tintColor) + imageView.setImageDrawable(superDrawable.drawable) + if (superDrawable.tintable) { + ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor)) + } else { + ImageViewCompat.setImageTintList(imageView, null) + } + } + } + } catch (e: Exception) { + Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e) + } + } + + /** + * Build a bitmap from a database [icon] + */ + fun getBitmapFromIcon(context: Context, + icon: IconImageDraw, + tintColor: Int = Color.BLACK): Bitmap? { + try { + val superDrawable = getIconSuperDrawable(context, + icon, + 24, + tintColor) + val bitmap = superDrawable.drawable.toBitmap() + // Tint bitmap if it's not a custom icon + if (superDrawable.tintable && bitmap.isMutable) { + Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply { + colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN) + }) + } + return bitmap + } catch (e: Exception) { + Log.e(RemoteViews::class.java.name, "Unable to create bitmap from icon", e) + } + return null + } + + /** + * Simple method to init the cache with the custom icon and be much faster next time + */ + private fun addToCustomCache(resources: Resources, iconDraw: IconImageDraw) { + val icon = iconDraw.getIconImageToDraw() + val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid) + if (customIconBinary != null + && customIconBinary.dataExists() + && !customIconMap.containsKey(icon.custom.uuid)) + getIconDrawable(resources, icon.custom, customIconBinary) + } + + /** + * Clear a specific icon from the cache + */ + fun clearFromCache(icon: IconImageCustom) { + customIconMap.remove(icon.uuid) + } + /** * Clear the cache of icons */ @@ -235,11 +236,11 @@ class IconDrawableFactory { customIconMap.clear() } - private class PatternIcon /** * Build a blankDrawable drawable * @param res Resource to build the drawable - */(res: Resources) { + */ + private class PatternIcon(res: Resources) { var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT) var width = -1 @@ -257,93 +258,38 @@ class IconDrawableFactory { */ class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false) - companion object { - - private val TAG = IconDrawableFactory::class.java.name - } - -} + /** + * Key class to retrieve a Drawable in the cache if it's tinted or not + */ + private inner class CacheKey(var resId: Int, var density: Int, var isTint: Boolean, var color: Int) { -/** - * Assign a default database icon to an ImageView and tint it with [tintColor] if needed - */ -fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, - tintColor: Int = Color.WHITE) { - try { - IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack -> - iconFactory.assignDrawableToImageView( - iconFactory.getIconSuperDrawable(context, - selectedIconPack.defaultIconId, - width, - selectedIconPack.tintable(), - tintColor), - this, - selectedIconPack.tintable(), - tintColor) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val cacheKey = other as CacheKey + return if (isTint) + resId == cacheKey.resId + && density == cacheKey.density + && cacheKey.isTint + && color == cacheKey.color + else + resId == cacheKey.resId + && density == cacheKey.density + && !cacheKey.isTint } - } catch (e: Exception) { - Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e) - } -} -/** - * Assign a database [icon] to an ImageView and tint it with [tintColor] if needed - */ -fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, - icon: IconImage, - tintColor: Int = Color.WHITE) { - try { - IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack -> - iconFactory.assignDrawableToImageView( - iconFactory.getIconSuperDrawable(context, - icon, - width, - true, - tintColor), - this, - selectedIconPack.tintable(), - tintColor) + override fun hashCode(): Int { + var result = resId + result = 31 * result + density + result = 31 * result + isTint.hashCode() + result = 31 * result + color + return result } - } catch (e: Exception) { - Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e) } -} -fun RemoteViews.assignDatabaseIcon(context: Context, - imageId: Int, - iconFactory: IconDrawableFactory, - icon: IconImage, - tintColor: Int = Color.BLACK) { - try { - iconFactory.assignDrawableToRemoteViews( - iconFactory.getIconSuperDrawable(context, - icon, - 24, - true, - tintColor), - this, - imageId, - tintColor) - } catch (e: Exception) { - Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) - } -} + companion object { -@RequiresApi(Build.VERSION_CODES.M) -fun createIconFromDatabaseIcon(context: Context, - iconFactory: IconDrawableFactory, - icon: IconImage, - tintColor: Int = Color.BLACK): Icon? { - try { - return iconFactory.assignDrawableToIcon( - iconFactory.getIconSuperDrawable(context, - icon, - 24, - true, - tintColor), - tintColor) - } catch (e: Exception) { - Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) + private val TAG = IconDrawableFactory::class.java.name } - return null + } diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconPack.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconPack.kt index 09ebd8f3f..7dc5985bf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconPack.kt +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconPack.kt @@ -78,7 +78,7 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) { // Build the list of icons var num = 0 - while (num <= NB_ICONS) { + while (num < NB_ICONS) { // To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp ) val resId = resources.getIdentifier( id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp", @@ -134,7 +134,6 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) { } companion object { - - private const val NB_ICONS = 68 + const val NB_ICONS = 69 } } diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.kt index f38321554..db72f9287 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconPackChooser.kt @@ -93,7 +93,7 @@ object IconPackChooser { fun setSelectedIconPack(iconPackIdString: String?) { for (iconPack in iconPackList) { if (iconPack.id == iconPackIdString) { - Database.getInstance().drawFactory.clearCache() + Database.getInstance().iconDrawableFactory.clearCache() iconPackSelected = iconPack break } 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 26b9ed5e4..5fcb9fb67 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryAttachmentState.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.model import android.os.Parcel import android.os.Parcelable import com.kunzisoft.keepass.database.element.Attachment -import com.kunzisoft.keepass.database.element.database.BinaryAttachment +import com.kunzisoft.keepass.database.element.database.BinaryByte import com.kunzisoft.keepass.utils.readEnum import com.kunzisoft.keepass.utils.writeEnum @@ -33,7 +33,7 @@ data class EntryAttachmentState(var attachment: Attachment, var previewState: AttachmentState = AttachmentState.NULL) : Parcelable { constructor(parcel: Parcel) : this( - parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()), + parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryByte()), parcel.readEnum() ?: StreamDirection.DOWNLOAD, parcel.readEnum() ?: AttachmentState.NULL, parcel.readInt(), diff --git a/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt index 9c4890ac8..491ed3838 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt @@ -3,14 +3,14 @@ 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 +import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID class GroupInfo : NodeInfo { var notes: String? = null init { - icon = IconImageStandard(FOLDER) + icon.standard = IconImageStandard(FOLDER_ID) } constructor(): super() diff --git a/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt index 9c2a1d252..731aa777a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt @@ -4,12 +4,11 @@ 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 icon: IconImage = IconImage() var creationTime: DateInstant = DateInstant() var lastModificationTime: DateInstant = DateInstant() var expires: Boolean = false 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 9e382b39a..204288c3e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt @@ -321,11 +321,9 @@ object OtpEntryFields { return try { // KeeOtp string format val query = breakDownKeyValuePairs(plainText) - otpElement.setBase32Secret(query[SEED_KEY] ?: "") otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD - otpElement.type = OtpType.TOTP true } catch (exception: Exception) { @@ -413,7 +411,7 @@ object OtpEntryFields { val output = HashMap() for (element in elements) { val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - output[pair[0]] = pair[1] + output[pair[0].toLowerCase(Locale.ENGLISH)] = pair[1] } return output } diff --git a/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt index adfd276ca..3f9ac7802 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt @@ -29,13 +29,10 @@ 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.readAllBytes -import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import kotlinx.coroutines.* import java.util.* import java.util.concurrent.CopyOnWriteArrayList @@ -86,7 +83,7 @@ class AttachmentFileNotificationService: LockNotificationService() { fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) } - override fun onBind(intent: Intent): IBinder? { + override fun onBind(intent: Intent): IBinder { return mActionTaskBinder } @@ -347,24 +344,27 @@ class AttachmentFileNotificationService: LockNotificationService() { when (streamDirection) { StreamDirection.UPLOAD -> { - uploadToDatabase( + BinaryDatabaseManager.uploadToDatabase( attachmentNotification.uri, - attachment.binaryAttachment, - contentResolver, 1024, + attachment.binaryData, + contentResolver, + { percent -> + publishProgress(percent) + }, { // Cancellation downloadState == AttachmentState.CANCELED } - ) { percent -> - publishProgress(percent) - } + ) } StreamDirection.DOWNLOAD -> { - downloadFromDatabase( + BinaryDatabaseManager.downloadFromDatabase( attachmentNotification.uri, - attachment.binaryAttachment, - contentResolver, 1024) { percent -> - publishProgress(percent) - } + attachment.binaryData, + contentResolver, + { percent -> + publishProgress(percent) + } + ) } } } catch (e: Exception) { @@ -396,57 +396,6 @@ class AttachmentFileNotificationService: LockNotificationService() { 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 - UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream -> - 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) - } - } - } - } - } - } - - fun uploadToDatabase(attachmentFromDownloadUri: Uri, - 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)?.use { inputStream -> - Database.getInstance().loadedCipherKey?.let { binaryCipherKey -> - binaryAttachment.getGzipOutputDataStream(binaryCipherKey).use { outputStream -> - inputStream.readAllBytes(bufferSize, canceled) { buffer -> - outputStream.write(buffer) - dataUploaded += buffer.size - try { - val percentDownload = (100 * dataUploaded / fileSize).toInt() - update?.invoke(percentDownload) - } catch (e: Exception) { - Log.e(TAG, "", e) - } - } - } - } - } - } - private fun publishProgress(percent: Int) { // Publish progress val currentTime = System.currentTimeMillis() diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index 14de85eb5..13cc63187 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -138,12 +138,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress val oldDatabaseModification = previousDatabaseInfo?.lastModification val newDatabaseModification = lastFileDatabaseInfo.lastModification + val oldDatabaseSize = previousDatabaseInfo?.size 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 + val conditionLastModification = + (oldDatabaseModification != null && newDatabaseModification != null + && oldDatabaseSize != null + && oldDatabaseModification > 0 && newDatabaseModification > 0 + && oldDatabaseSize > 0 && oldDatabaseModification < newDatabaseModification && mLastLocalSaveTime + 10000 < newDatabaseModification) 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 b9ebbab67..0fcbeb393 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt @@ -385,7 +385,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } } if (styleEnabled) { - Stylish.assignStyle(styleIdString) + Stylish.assignStyle(activity, styleIdString) // Relaunch the current activity to redraw theme (activity as? SettingsActivity?)?.apply { keepCurrentScreen() @@ -397,6 +397,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { styleEnabled } + findPreference(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ -> + (activity as? SettingsActivity?)?.apply { + keepCurrentScreen() + startActivity(intent) + finish() + activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } + true + } + findPreference(getString(R.string.setting_icon_pack_choose_key))?.setOnPreferenceChangeListener { _, newValue -> var iconPackEnabled = true val iconPackId = newValue as String diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index f2c166d9e..ee5405f60 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -26,6 +26,7 @@ import android.net.Uri import androidx.preference.PreferenceManager import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -132,6 +133,21 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.show_uuid_default)) } + fun getStyle(context: Context): String { + val stylishPrefKey = context.getString(R.string.setting_style_key) + val defaultStyleString = context.getString(R.string.list_style_name_light) + val styleString = PreferenceManager.getDefaultSharedPreferences(context) + .getString(stylishPrefKey, defaultStyleString) + ?: defaultStyleString + return Stylish.retrieveEquivalentLightStyle(context, styleString) + } + + fun getStyleBrightness(context: Context): String? { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getString(context.getString(R.string.setting_style_brightness_key), + context.resources.getString(R.string.list_style_brightness_follow_system)) + } + /** * Retrieve the text size in % (1 for 100%) */ diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt new file mode 100644 index 000000000..8a6387e12 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt @@ -0,0 +1,193 @@ +package com.kunzisoft.keepass.tasks + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.database.BinaryData +import com.kunzisoft.keepass.stream.readAllBytes +import com.kunzisoft.keepass.utils.UriUtil +import kotlinx.coroutines.* +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import kotlin.math.ceil +import kotlin.math.ln +import kotlin.math.max +import kotlin.math.pow + +object BinaryDatabaseManager { + + fun downloadFromDatabase(attachmentToUploadUri: Uri, + binaryData: BinaryData, + contentResolver: ContentResolver, + update: ((percent: Int)->Unit)? = null, + canceled: ()-> Boolean = { false }, + bufferSize: Int = DEFAULT_BUFFER_SIZE) { + UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream -> + downloadFromDatabase(outputStream, binaryData, update, canceled, bufferSize) + } + } + + private fun downloadFromDatabase(outputStream: OutputStream, + binaryData: BinaryData, + update: ((percent: Int)->Unit)? = null, + canceled: ()-> Boolean = { false }, + bufferSize: Int = DEFAULT_BUFFER_SIZE) { + val fileSize = binaryData.getSize() + var dataDownloaded = 0L + Database.getInstance().loadedCipherKey?.let { binaryCipherKey -> + binaryData.getUnGzipInputDataStream(binaryCipherKey).use { inputStream -> + inputStream.readAllBytes(bufferSize, canceled) { buffer -> + outputStream.write(buffer) + dataDownloaded += buffer.size + try { + val percentDownload = (100 * dataDownloaded / fileSize).toInt() + update?.invoke(percentDownload) + } catch (e: Exception) { + Log.w(TAG, "Unable to call update callback during download", e) + } + } + } + } + } + + fun uploadToDatabase(attachmentFromDownloadUri: Uri, + binaryData: BinaryData, + contentResolver: ContentResolver, + update: ((percent: Int)->Unit)? = null, + canceled: ()-> Boolean = { false }, + bufferSize: Int = DEFAULT_BUFFER_SIZE) { + val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0 + UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream -> + uploadToDatabase(inputStream, fileSize, binaryData, update, canceled, bufferSize) + } + } + + private fun uploadToDatabase(inputStream: InputStream, + fileSize: Long, + binaryData: BinaryData, + update: ((percent: Int)->Unit)? = null, + canceled: ()-> Boolean = { false }, + bufferSize: Int = DEFAULT_BUFFER_SIZE) { + var dataUploaded = 0L + Database.getInstance().loadedCipherKey?.let { binaryCipherKey -> + binaryData.getGzipOutputDataStream(binaryCipherKey).use { outputStream -> + inputStream.readAllBytes(bufferSize, canceled) { buffer -> + outputStream.write(buffer) + dataUploaded += buffer.size + try { + val percentDownload = (100 * dataUploaded / fileSize).toInt() + update?.invoke(percentDownload) + } catch (e: Exception) { + Log.w(TAG, "Unable to call update callback during upload", e) + } + } + } + } + } + + fun resizeBitmapAndStoreDataInBinaryFile(contentResolver: ContentResolver, + bitmapUri: Uri?, + binaryData: BinaryData?) { + try { + binaryData?.let { + UriUtil.getUriInputStream(contentResolver, bitmapUri)?.use { inputStream -> + BitmapFactory.decodeStream(inputStream)?.let { bitmap -> + val bitmapResized = bitmap.resize(DEFAULT_ICON_WIDTH) + val byteArrayOutputStream = ByteArrayOutputStream() + bitmapResized?.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream) + val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() + val byteArrayInputStream = ByteArrayInputStream(bitmapData) + uploadToDatabase( + byteArrayInputStream, + bitmapData.size.toLong(), + binaryData + ) + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to resize bitmap to store it in binary", e) + } + } + + + /** + * reduces the size of the image + * @param image + * @param maxSize + * @return + */ + private fun Bitmap.resize(maxSize: Int): Bitmap? { + var width = this.width + var height = this.height + val bitmapRatio = width.toFloat() / height.toFloat() + if (bitmapRatio > 1) { + width = maxSize + height = (width / bitmapRatio).toInt() + } else { + height = maxSize + width = (height * bitmapRatio).toInt() + } + return Bitmap.createScaledBitmap(this, width, height, true) + } + + fun loadBitmap(binaryData: BinaryData, + binaryCipherKey: Database.LoadedKey?, + maxWidth: Int, + actionOnFinish: (Bitmap?) -> Unit) { + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + val asyncResult: Deferred = async { + runCatching { + binaryCipherKey?.let { binaryKey -> + val bitmap: Bitmap? = decodeSampledBitmap(binaryData, + binaryKey, + maxWidth) + bitmap + } + }.getOrNull() + } + withContext(Dispatchers.Main) { + actionOnFinish(asyncResult.await()) + } + } + } + } + + private fun decodeSampledBitmap(binaryData: BinaryData, + binaryCipherKey: Database.LoadedKey, + maxWidth: Int): Bitmap? { + // First decode with inJustDecodeBounds=true to check dimensions + return BitmapFactory.Options().run { + try { + inJustDecodeBounds = true + binaryData.getUnGzipInputDataStream(binaryCipherKey).use { + BitmapFactory.decodeStream(it, null, this) + } + // Calculate inSampleSize + var scale = 1 + if (outHeight > maxWidth || outWidth > maxWidth) { + scale = 2.0.pow(ceil(ln(maxWidth / max(outHeight, outWidth).toDouble()) / ln(0.5))).toInt() + } + inSampleSize = scale + + // Decode bitmap with inSampleSize set + inJustDecodeBounds = false + binaryData.getUnGzipInputDataStream(binaryCipherKey).use { + BitmapFactory.decodeStream(it, null, this) + } + } catch (e: Exception) { + null + } + } + } + + private const val DEFAULT_ICON_WIDTH = 64 + + private val TAG = BinaryDatabaseManager::class.java.name +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.kt b/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.kt index cf7a6af8a..b123fce8d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.kt @@ -42,8 +42,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, : RelativeLayout(context, attrs, defStyle) { var addButtonView: FloatingActionButton? = null - private var addEntryView: View? = null - private var addGroupView: View? = null + private lateinit var addEntryView: View + private lateinit var addGroupView: View private var addEntryEnable: Boolean = false private var addGroupEnable: Boolean = false @@ -82,8 +82,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, animationDuration = 300L viewButtonMenuAnimation = AddButtonAnimation(addButtonView) - viewMenuAnimationAddEntry = ViewMenuAnimation(addEntryView, 0L, 150L) - viewMenuAnimationAddGroup = ViewMenuAnimation(addGroupView, 150L, 0L) + viewMenuAnimationAddEntry = ViewMenuAnimation(addEntryView, 150L, 0L) + viewMenuAnimationAddGroup = ViewMenuAnimation(addGroupView, 0L, 150L) allowAction = true state = State.CLOSE @@ -111,8 +111,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, val viewEntryRect = Rect() val viewGroupRect = Rect() addButtonView?.getGlobalVisibleRect(viewButtonRect) - addEntryView?.getGlobalVisibleRect(viewEntryRect) - addGroupView?.getGlobalVisibleRect(viewGroupRect) + addEntryView.getGlobalVisibleRect(viewEntryRect) + addGroupView.getGlobalVisibleRect(viewGroupRect) if (!(viewButtonRect.contains(event.rawX.toInt(), event.rawY.toInt()) && viewEntryRect.contains(event.rawX.toInt(), event.rawY.toInt()) && viewGroupRect.contains(event.rawX.toInt(), event.rawY.toInt()))) { @@ -165,8 +165,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, */ fun enableAddEntry(enable: Boolean) { this.addEntryEnable = enable - if (enable && addEntryView != null && addEntryView!!.visibility != View.VISIBLE) - addEntryView!!.visibility = View.INVISIBLE + if (enable && addEntryView.visibility != View.VISIBLE) + addEntryView.visibility = View.INVISIBLE disableViewIfNoAddAvailable() } @@ -176,13 +176,13 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, */ fun enableAddGroup(enable: Boolean) { this.addGroupEnable = enable - if (enable && addGroupView != null && addGroupView!!.visibility != View.VISIBLE) - addGroupView?.visibility = View.INVISIBLE + if (enable && addGroupView.visibility != View.VISIBLE) + addGroupView.visibility = View.INVISIBLE disableViewIfNoAddAvailable() } private fun disableViewIfNoAddAvailable() { - visibility = if (!addEntryEnable || !addGroupEnable) { + visibility = if (!addEntryEnable && !addGroupEnable) { View.GONE } else { View.VISIBLE @@ -191,7 +191,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, fun setAddGroupClickListener(onClickListener: OnClickListener) { if (addGroupEnable) - addGroupView?.setOnClickListener { view -> + addGroupView.setOnClickListener { view -> onClickListener.onClick(view) closeButtonIfOpen() } @@ -199,11 +199,11 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, fun setAddEntryClickListener(onClickListener: OnClickListener) { if (addEntryEnable) { - addEntryView?.setOnClickListener { view -> + addEntryView.setOnClickListener { view -> onClickListener.onClick(view) closeButtonIfOpen() } - addEntryView?.setOnClickListener { view -> + addEntryView.setOnClickListener { view -> onClickListener.onClick(view) closeButtonIfOpen() } @@ -248,7 +248,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, override fun onAnimationCancel(view: View) {} - internal fun startAnimation() { + fun startAnimation() { view?.let { view -> if (!isRotate) { ViewCompat.animate(view) @@ -298,7 +298,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context, override fun onAnimationCancel(view: View) {} - internal fun startAnimation() { + fun startAnimation() { view?.let { view -> if (view.visibility == View.VISIBLE) { // In diff --git a/app/src/main/java/com/kunzisoft/keepass/view/SpecialModeView.kt b/app/src/main/java/com/kunzisoft/keepass/view/SpecialModeView.kt index 030d330b2..77ab72795 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/SpecialModeView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/SpecialModeView.kt @@ -20,9 +20,14 @@ package com.kunzisoft.keepass.view import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.util.AttributeSet +import android.util.TypedValue import android.view.View +import androidx.annotation.ColorInt import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat import com.kunzisoft.keepass.R class SpecialModeView @JvmOverloads constructor(context: Context, @@ -31,7 +36,13 @@ class SpecialModeView @JvmOverloads constructor(context: Context, : Toolbar(context, attrs, defStyle) { init { - setNavigationIcon(R.drawable.ic_arrow_back_white_24dp) + ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_white_24dp)?.let { closeDrawable -> + val typedValue = TypedValue() + context.theme.resolveAttribute(R.attr.colorControlNormal, typedValue, true) + @ColorInt val colorControl = typedValue.data + closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP) + navigationIcon = closeDrawable + } title = resources.getString(R.string.selection_mode) } 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 cc86a0296..7c103a828 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt @@ -19,19 +19,25 @@ */ package com.kunzisoft.keepass.view +import android.annotation.SuppressLint import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.util.AttributeSet +import android.util.TypedValue import android.view.Menu import android.view.MenuInflater import android.view.View +import androidx.annotation.ColorInt import androidx.appcompat.view.ActionMode import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat import com.kunzisoft.keepass.R class ToolbarAction @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyle: Int = R.attr.actionToolbarAppearance) + defStyle: Int = androidx.appcompat.R.attr.toolbarStyle) : Toolbar(context, attrs, defStyle) { private var mActionModeCallback: ActionMode.Callback? = null @@ -39,7 +45,13 @@ class ToolbarAction @JvmOverloads constructor(context: Context, private var isOpen = false init { - setNavigationIcon(R.drawable.ic_close_white_24dp) + ContextCompat.getDrawable(context, R.drawable.ic_close_white_24dp)?.let { closeDrawable -> + val typedValue = TypedValue() + context.theme.resolveAttribute(R.attr.colorControlNormal, typedValue, true) + @ColorInt val colorControl = typedValue.data + closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP) + navigationIcon = closeDrawable + } } fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode { @@ -106,6 +118,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context, override fun setCustomView(view: View?) {} + @SuppressLint("RestrictedApi") override fun getMenuInflater(): MenuInflater { return SupportMenuInflater(toolbarAction.context) } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt new file mode 100644 index 000000000..079a345db --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt @@ -0,0 +1,89 @@ +package com.kunzisoft.keepass.viewmodels + +import android.os.Parcel +import android.os.Parcelable +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.kunzisoft.keepass.database.element.icon.IconImageCustom +import com.kunzisoft.keepass.database.element.icon.IconImageStandard + + +class IconPickerViewModel: ViewModel() { + + val standardIconPicked: MutableLiveData by lazy { + MutableLiveData() + } + + val customIconPicked: MutableLiveData by lazy { + MutableLiveData() + } + + val customIconsSelected: MutableLiveData> by lazy { + MutableLiveData>() + } + + val customIconAdded: MutableLiveData by lazy { + MutableLiveData() + } + + val customIconRemoved: MutableLiveData by lazy { + MutableLiveData() + } + + fun pickStandardIcon(icon: IconImageStandard) { + standardIconPicked.value = icon + } + + fun pickCustomIcon(icon: IconImageCustom) { + customIconPicked.value = icon + } + + fun selectCustomIcons(icons: List) { + customIconsSelected.value = icons + } + + fun deselectAllCustomIcons() { + customIconsSelected.value = listOf() + } + + fun addCustomIcon(customIcon: IconCustomState) { + customIconAdded.value = customIcon + } + + fun removeCustomIcon(customIcon: IconCustomState) { + customIconRemoved.value = customIcon + } + + data class IconCustomState(var iconCustom: IconImageCustom? = null, + var error: Boolean = true, + var errorStringId: Int = -1, + var errorConsumed: Boolean = false): Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readParcelable(IconImageCustom::class.java.classLoader), + parcel.readByte() != 0.toByte(), + parcel.readInt(), + parcel.readByte() != 0.toByte()) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(iconCustom, flags) + parcel.writeByte(if (error) 1 else 0) + parcel.writeInt(errorStringId) + parcel.writeByte(if (errorConsumed) 1 else 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): IconCustomState { + return IconCustomState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/color/background_button_color_accent.xml b/app/src/main/res/color/background_button_color_accent.xml index de359a121..9af1c0e0d 100644 --- a/app/src/main/res/color/background_button_color_accent.xml +++ b/app/src/main/res/color/background_button_color_accent.xml @@ -1,7 +1,7 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/color/background_button_color_primary.xml b/app/src/main/res/color/background_button_color_primary.xml index 043b2cd29..31a28d56a 100644 --- a/app/src/main/res/color/background_button_color_primary.xml +++ b/app/src/main/res/color/background_button_color_primary.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/background_button_color_secondary.xml b/app/src/main/res/color/background_button_color_secondary.xml index 0903962ac..6824d6795 100644 --- a/app/src/main/res/color/background_button_color_secondary.xml +++ b/app/src/main/res/color/background_button_color_secondary.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/color/background_special_button_color.xml b/app/src/main/res/color/background_special_button_color.xml index fdbfb578a..7e4991bbc 100644 --- a/app/src/main/res/color/background_special_button_color.xml +++ b/app/src/main/res/color/background_special_button_color.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/entry_subtitle_color.xml b/app/src/main/res/color/entry_subtitle_color.xml index 4bf1f9e4e..a7ac5fa81 100644 --- a/app/src/main/res/color/entry_subtitle_color.xml +++ b/app/src/main/res/color/entry_subtitle_color.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/entry_title_color.xml b/app/src/main/res/color/entry_title_color.xml index ccc9d8edf..cc8ab381d 100644 --- a/app/src/main/res/color/entry_title_color.xml +++ b/app/src/main/res/color/entry_title_color.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/group_subtitle_color.xml b/app/src/main/res/color/group_subtitle_color.xml index 4bf1f9e4e..a7ac5fa81 100644 --- a/app/src/main/res/color/group_subtitle_color.xml +++ b/app/src/main/res/color/group_subtitle_color.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/group_title_color.xml b/app/src/main/res/color/group_title_color.xml index b32473384..d5cc012a7 100644 --- a/app/src/main/res/color/group_title_color.xml +++ b/app/src/main/res/color/group_title_color.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_button.xml b/app/src/main/res/color/text_color_button.xml index 7c2d988bf..9203a4a5d 100644 --- a/app/src/main/res/color/text_color_button.xml +++ b/app/src/main/res/color/text_color_button.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_light.xml b/app/src/main/res/color/text_color_light.xml index 26144d85e..83fbc9bbf 100644 --- a/app/src/main/res/color/text_color_light.xml +++ b/app/src/main/res/color/text_color_light.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_night.xml b/app/src/main/res/color/text_color_night.xml index b979251e1..b452f1fa0 100644 --- a/app/src/main/res/color/text_color_night.xml +++ b/app/src/main/res/color/text_color_night.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_secondary_light.xml b/app/src/main/res/color/text_color_secondary_light.xml index 3b0f35df9..7005319e0 100644 --- a/app/src/main/res/color/text_color_secondary_light.xml +++ b/app/src/main/res/color/text_color_secondary_light.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_secondary_night.xml b/app/src/main/res/color/text_color_secondary_night.xml index 2815ece81..ea11472bf 100644 --- a/app/src/main/res/color/text_color_secondary_night.xml +++ b/app/src/main/res/color/text_color_secondary_night.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_item_selection.xml b/app/src/main/res/drawable/background_item_selection.xml index 8d6ff044a..eb60a1034 100644 --- a/app/src/main/res/drawable/background_item_selection.xml +++ b/app/src/main/res/drawable/background_item_selection.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/drawable/background_progress_circle.xml b/app/src/main/res/drawable/background_progress_circle.xml index 95d7cb50a..649f42f5b 100644 --- a/app/src/main/res/drawable/background_progress_circle.xml +++ b/app/src/main/res/drawable/background_progress_circle.xml @@ -8,5 +8,5 @@ android:top="-2dp" /> + android:color="@color/white_grey_darker"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml index 59bfb528f..2237aaa99 100644 --- a/app/src/main/res/drawable/background_splash.xml +++ b/app/src/main/res/drawable/background_splash.xml @@ -1,7 +1,7 @@ + android:drawable="@color/grey_dark"/> diff --git a/app/src/main/res/drawable/btn_special_start_bottom_stroke.xml b/app/src/main/res/drawable/btn_special_start_bottom_stroke.xml index b5567a25d..59e24d00a 100644 --- a/app/src/main/res/drawable/btn_special_start_bottom_stroke.xml +++ b/app/src/main/res/drawable/btn_special_start_bottom_stroke.xml @@ -5,7 +5,7 @@ + android:color="@color/white_grey" /> + + diff --git a/app/src/main/res/drawable/key_background_normal.xml b/app/src/main/res/drawable/key_background_normal.xml index c24861d79..c6cdf17b6 100644 --- a/app/src/main/res/drawable/key_background_normal.xml +++ b/app/src/main/res/drawable/key_background_normal.xml @@ -25,6 +25,6 @@ android:right="0dp" android:top="12dp" android:bottom="12dp"/> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/key_background_pressed.xml b/app/src/main/res/drawable/key_background_pressed.xml index a6c2d5eed..18d70899e 100644 --- a/app/src/main/res/drawable/key_background_pressed.xml +++ b/app/src/main/res/drawable/key_background_pressed.xml @@ -25,6 +25,6 @@ android:right="0dp" android:top="12dp" android:bottom="12dp"/> - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index 93f4eda35..e96c41ab3 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -67,7 +67,6 @@ android:layout_height="?attr/actionBarSize" android:background="@color/transparent" android:theme="?attr/toolbarAppearance" - 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 c24debc93..f5faa0949 100644 --- a/app/src/main/res/layout/activity_entry_edit.xml +++ b/app/src/main/res/layout/activity_entry_edit.xml @@ -17,78 +17,83 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - - + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/entry_edit_bottom_bar"> - - - + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> + + - - - - - + android:layout_height="match_parent" + android:scrollbarStyle="insideOverlay" + android:scrollbars="none" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + + + android:theme="?attr/toolbarActionAppearance" + android:layout_gravity="bottom" + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - \ No newline at end of file + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_file_selection.xml b/app/src/main/res/layout/activity_file_selection.xml index 716162170..196eb1a29 100644 --- a/app/src/main/res/layout/activity_file_selection.xml +++ b/app/src/main/res/layout/activity_file_selection.xml @@ -30,9 +30,7 @@ android:id="@+id/special_mode_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:theme="?attr/specialToolbarAppearance" - app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title" - app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle" + android:theme="?attr/toolbarSpecialAppearance" app:layout_constraintTop_toTopOf="parent" /> + android:theme="?attr/toolbarHomeAppearance" /> diff --git a/app/src/main/res/layout/activity_group.xml b/app/src/main/res/layout/activity_group.xml index 8073ea918..e7bd96282 100644 --- a/app/src/main/res/layout/activity_group.xml +++ b/app/src/main/res/layout/activity_group.xml @@ -29,9 +29,7 @@ android:id="@+id/special_mode_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:theme="?attr/specialToolbarAppearance" - app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title" - app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle" + android:theme="?attr/toolbarSpecialAppearance" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/activity_icon_picker.xml b/app/src/main/res/layout/activity_icon_picker.xml new file mode 100644 index 000000000..9d3acd0ce --- /dev/null +++ b/app/src/main/res/layout/activity_icon_picker.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_password.xml index ddb806596..874e3c8ad 100644 --- a/app/src/main/res/layout/activity_password.xml +++ b/app/src/main/res/layout/activity_password.xml @@ -31,9 +31,7 @@ android:id="@+id/special_mode_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:theme="?attr/specialToolbarAppearance" - app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title" - app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle" + android:theme="?attr/toolbarSpecialAppearance" app:layout_constraintTop_toTopOf="parent" /> + app:endIconMode="password_toggle" + app:endIconTint="?attr/colorAccent"> + diff --git a/app/src/main/res/layout/fragment_browser_install.xml b/app/src/main/res/layout/fragment_browser_install.xml index 5aaa147aa..a557b8afa 100644 --- a/app/src/main/res/layout/fragment_browser_install.xml +++ b/app/src/main/res/layout/fragment_browser_install.xml @@ -30,17 +30,11 @@ android:text="@string/file_manager_install_description" android:gravity="center" android:layout_marginBottom="12dp" /> - -