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