Merge branch 'release/2.9.14'

This commit is contained in:
J-Jamet
2021-03-20 11:42:33 +01:00
196 changed files with 4712 additions and 2240 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<IconImageCustom> = 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<IconImage>(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<ViewGroup>(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<IconPickerViewModel.IconCustomState?> = 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)
}
}
}

View File

@@ -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<Attachment>(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) {

View File

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

View File

@@ -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<View>(R.id.credentials_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
}

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<GridView>(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<ImageView>(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")
}
}
}

View File

@@ -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<View>(R.id.otp_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<IconImageCustom>() {
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())
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<T: IconImageDraw> : StylishFragment(),
IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
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<T>(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
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
class IconStandardFragment : IconFragment<IconImageStandard>() {
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) {}
}

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
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

View File

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

View File

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

View File

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

View File

@@ -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<I: IconImageDraw>(val context: Context, private val tintIcon: Int)
: RecyclerView.Adapter<IconPickerAdapter<I>.CustomIconViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val iconList = ArrayList<I>()
var iconDrawableFactory: IconDrawableFactory? = null
var iconPickerListener: IconPickerListener<I>? = 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<I>) {
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<I> {
return iconList.filter { it.selected }
}
fun clear() {
iconList.clear()
}
fun setList(icons: List<I>) {
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<I: IconImageDraw> {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

@@ -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<Attachment>(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)

View File

@@ -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<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
abstract fun getPwNodeId(): NodeId<EntryId>
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))

View File

@@ -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<EntryKDB>() {
@@ -30,9 +29,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
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,

View File

@@ -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<EntryKDBX>() {
@@ -34,9 +34,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
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<EntryKDBX>() {
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()) {

View File

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

View File

@@ -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<Entry>() {
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)
}
}

View File

@@ -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<Group> {
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<Group> {
}
}
/*
------------
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<Group> {
entryKDBX?.stopToManageFieldReferences()
}
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>()
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<Group> {
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<Entry> {
@@ -390,8 +369,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
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<Group> {
// 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<Group> {
url = newEntryInfo.url
notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool ->
database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment ->
putAttachment(attachment, binaryPool)
}

View File

@@ -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<Group, Entry> {
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<Group, Entry> {
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<Group, Entry> {
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<Group, Entry> {
}
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<Group, Entry> {
groupKDBX?.removeChildren()
}
override fun allowAddEntryIfIsRoot(): Boolean {
return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false
}
val allowAddEntryIfIsRoot: Boolean
get() = groupKDBX != null
val allowAddNoteInGroup: Boolean
get() = groupKDBX != null
/*
------------

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.database
class AttachmentPool : BinaryPool<Int>() {
/**
* 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
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
override fun createFromParcel(parcel: Parcel): BinaryByte {
return BinaryByte(parcel)
}
override fun newArray(size: Int): Array<BinaryByte?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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
}
}

View File

@@ -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
mLength = parcel.readLong()
mBinaryHash = parcel.readInt()
}
@Throws(IOException::class)
fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return buildInputStream(dataFile!!, cipherKey)
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return buildInputStream(mDataFile, cipherKey)
}
@Throws(IOException::class)
fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return buildOutputStream(dataFile!!, cipherKey)
}
@Throws(IOException::class)
fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return if (isCompressed) {
GZIPInputStream(getInputDataStream(cipherKey))
} else {
getInputDataStream(cipherKey)
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream(cipherKey))
} else {
getOutputDataStream(cipherKey)
}
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 {
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
}
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int {
return 0
}
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 getSize(): Long {
return mLength
}
/**
* Custom OutputStream to calculate the size of binary file
* 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 mDataFile.toString()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeString(mDataFile?.absolutePath)
dest.writeLong(mLength)
dest.writeInt(mBinaryHash)
}
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 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<BinaryAttachment> = object : Parcelable.Creator<BinaryAttachment> {
override fun createFromParcel(parcel: Parcel): BinaryAttachment {
return BinaryAttachment(parcel)
val CREATOR: Parcelable.Creator<BinaryFile> = object : Parcelable.Creator<BinaryFile> {
override fun createFromParcel(parcel: Parcel): BinaryFile {
return BinaryFile(parcel)
}
override fun newArray(size: Int): Array<BinaryAttachment?> {
override fun newArray(size: Int): Array<BinaryFile?> {
return arrayOfNulls(size)
}
}

View File

@@ -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<Int, BinaryAttachment>()
abstract class BinaryPool<T> {
protected val pool = LinkedHashMap<T, BinaryData>()
// 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<T> {
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<KeyBinary> {
val keyBinaryList = ArrayList<KeyBinary>()
protected fun orderedBinariesWithoutDuplication(condition: ((binary: BinaryData) -> Boolean) = { true })
: List<KeyBinary<T>> {
val keyBinaryList = ArrayList<KeyBinary<T>>()
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<T>) -> Unit,
conditionToAdd: (binary: BinaryData) -> Boolean) {
orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
action.invoke(keyBinary)
}
}
fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> 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<T>(val binary: BinaryData, key: T) {
val keys = HashSet<T>()
init {
addKey(key)
}
fun addKey(key: T) {
keys.add(key)
}
}
companion object {
private val TAG = BinaryPool::class.java.name
}
}

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.database.element.database
import java.util.*
class CustomIconPool : BinaryPool<UUID>() {
override fun findUnusedKey(): UUID {
var newUUID = UUID.randomUUID()
while (pool.containsKey(newUUID)) {
newUUID = UUID.randomUUID()
}
return newUUID
}
}

View File

@@ -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<Int, UUID, GroupKDB, EntryKDB>() {
private var kdfListV3: MutableList<KdfEngine> = 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<Int, UUID, GroupKDB, EntryKDB>() {
getGroupById(backupGroupId)
}
override val kdfEngine: KdfEngine?
override val kdfEngine: KdfEngine
get() = kdfListV3[0]
override val kdfAvailableList: List<KdfEngine>
@@ -175,6 +177,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
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<Int, UUID, GroupKDB, EntryKDB>() {
// 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<Int, UUID, GroupKDB, EntryKDB>() {
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 {

View File

@@ -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<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>()
val customIcons = ArrayList<IconImageCustom>()
val customData = HashMap<String, String>()
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<UUID, UUID, GroupKDBX, EntryKDBX> {
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<UUID, UUID, GroupKDBX, EntryKDBX> {
}
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<UUID, UUID, GroupKDBX, EntryKDBX> {
}
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<UUID, UUID, GroupKDBX, EntryKDBX> {
this.dataEngine = dataEngine
}
fun getCustomIcons(): List<IconImageCustom> {
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<String, String> {
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<UUID, UUID, GroupKDBX, EntryKDBX> {
}
override fun containsCustomData(): Boolean {
return getCustomData().isNotEmpty()
return customData.isNotEmpty()
}
@Throws(IOException::class)
@@ -550,7 +562,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// 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<UUID, UUID, GroupKDBX, EntryKDBX> {
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<BinaryAttachment>()
fun removeUnlinkedAttachment(binary: BinaryData, clear: Boolean) {
val listBinaries = ArrayList<BinaryData>()
listBinaries.add(binary)
removeUnlinkedAttachments(listBinaries, clear)
}
@@ -652,11 +661,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
removeUnlinkedAttachments(emptyList(), clear)
}
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
// Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryAttachment>()
val binariesToRemove = ArrayList<BinaryData>()
if (binaries.isEmpty()) {
binaryPool.doForEachBinary { binary ->
binaryPool.doForEachBinary { _, binary ->
binariesToRemove.add(binary)
}
} else {
@@ -666,7 +675,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean {
node.getAttachments(binaryPool, true).forEach {
binariesToRemove.remove(it.binaryAttachment)
binariesToRemove.remove(it.binaryData)
}
return binariesToRemove.isNotEmpty()
}

View File

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

View File

@@ -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<Int, UUID, GroupKDB, EntryKDB>, 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<Int, UUID, GroupKDB, EntryKDB>, 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<UUID> {
@@ -88,7 +89,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, 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<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
fun putAttachment(attachment: Attachment) {
this.binaryDescription = attachment.name
this.binaryData = attachment.binaryAttachment
this.binaryData = attachment.binaryData
}
fun removeAttachment(attachment: Attachment? = null) {

View File

@@ -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<UUID, UUID, GroupKDBX, EntryKDBX>, 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<String, String>()
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
@@ -72,7 +56,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
var additional = ""
var tags = ""
fun getSize(binaryPool: BinaryPool): Long {
fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) {
@@ -80,7 +64,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
size += entry.value.length().toLong()
}
size += getAttachmentsSize(binaryPool)
size += getAttachmentsSize(attachmentPool)
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
@@ -89,7 +73,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
for (entry in history) {
size += entry.getSize(binaryPool)
size += entry.getSize(attachmentPool)
}
size += overrideURL.length.toLong()
@@ -103,7 +87,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel)
@@ -121,7 +104,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData)
@@ -143,7 +125,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
*/
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
super.updateWith(source)
iconCustom = IconImageCustom(source.iconCustom)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map
@@ -281,16 +262,16 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
/**
* It's a list because history labels can be defined multiple times
*/
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
val entryAttachmentList = ArrayList<Attachment>()
for ((label, poolId) in binaries) {
binaryPool[poolId]?.let { binary ->
attachmentPool[poolId]?.let { binary ->
entryAttachmentList.add(Attachment(label, binary))
}
}
if (inHistory) {
history.forEach {
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
entryAttachmentList.addAll(it.getAttachments(attachmentPool, false))
}
}
return entryAttachmentList
@@ -300,8 +281,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return binaries.isNotEmpty()
}
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
binaries[attachment.name] = attachmentPool.put(attachment.binaryData)
}
fun removeAttachment(attachment: Attachment) {
@@ -312,11 +293,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
binaries.clear()
}
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
private fun getAttachmentsSize(attachmentPool: AttachmentPool): Long {
var size = 0L
for ((label, poolId) in binaries) {
size += label.length.toLong()
size += binaryPool[poolId]?.length ?: 0
size += attachmentPool[poolId]?.getSize() ?: 0
}
return size
}
@@ -333,7 +314,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
history.add(entry)
}
fun removeEntryFromHistory(position: Int): EntryKDBX? {
fun removeEntryFromHistory(position: Int): EntryKDBX {
return history.removeAt(position)
}

View File

@@ -82,10 +82,6 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
this.nodeId = NodeIdInt(groupId)
}
override fun allowAddEntryIfIsRoot(): Boolean {
return false
}
companion object {
@JvmField

View File

@@ -21,37 +21,18 @@ package com.kunzisoft.keepass.database.element.group
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.HashMap
import java.util.UUID
import java.util.*
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
// TODO Encapsulate
override var icon: IconImage
get() {
return if (iconCustom.isUnknown)
super.icon
else
iconCustom
}
set(value) {
if (value is IconImageStandard)
iconCustom = IconImageCustom.UNKNOWN_ICON
super.icon = value
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
private val customData = HashMap<String, String>()
var notes = ""
@@ -77,7 +58,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
@@ -101,7 +81,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
@@ -115,7 +94,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
fun updateWith(source: GroupKDBX) {
super.updateWith(source)
iconCustom = IconImageCustom(source.iconCustom)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map
@@ -147,10 +125,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return customData.isNotEmpty()
}
override fun allowAddEntryIfIsRoot(): Boolean {
return true
}
companion object {
@JvmField

View File

@@ -38,8 +38,6 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
fun removeChildren()
fun allowAddEntryIfIsRoot(): Boolean
@Suppress("UNCHECKED_CAST")
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
groupHandler: NodeHandler<Group>) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -19,19 +19,69 @@
*/
package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.Parcelable
abstract class IconImage protected constructor() : Parcelable {
class IconImage() : IconImageDraw(), Parcelable {
abstract val iconId: Int
abstract val isUnknown: Boolean
abstract val isMetaStreamIcon: Boolean
var standard: IconImageStandard = IconImageStandard()
var custom: IconImageCustom = IconImageCustom()
constructor(iconImageStandard: IconImageStandard) : this() {
this.standard = iconImageStandard
}
constructor(iconImageCustom: IconImageCustom) : this() {
this.custom = iconImageCustom
}
constructor(iconImageStandard: IconImageStandard,
iconImageCustom: IconImageCustom) : this() {
this.standard = iconImageStandard
this.custom = iconImageCustom
}
constructor(parcel: Parcel) : this() {
standard = parcel.readParcelable(IconImageStandard::class.java.classLoader) ?: standard
custom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: custom
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(standard, flags)
parcel.writeParcelable(custom, flags)
}
override fun describeContents(): Int {
return 0
}
companion object {
const val UNKNOWN_ID = -1
override fun getIconImageToDraw(): IconImage {
return this
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is IconImage) return false
if (standard != other.standard) return false
if (custom != other.custom) return false
return true
}
override fun hashCode(): Int {
var result = standard.hashCode()
result = 31 * result + custom.hashCode()
return result
}
companion object CREATOR : Parcelable.Creator<IconImage> {
override fun createFromParcel(parcel: Parcel): IconImage {
return IconImage(parcel)
}
override fun newArray(size: Int): Array<IconImage?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -22,39 +22,30 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.*
import java.util.UUID
class IconImageCustom : Parcelable, IconImageDraw {
class IconImageCustom : IconImage {
var uuid: UUID
val uuid: UUID
@Transient
var imageData: ByteArray = ByteArray(0)
constructor(uuid: UUID, data: ByteArray) : super() {
this.uuid = uuid
this.imageData = data
constructor() {
uuid = DatabaseVersioned.UUID_ZERO
}
constructor(uuid: UUID) : super() {
constructor(uuid: UUID) {
this.uuid = uuid
this.imageData = ByteArray(0)
}
constructor(icon: IconImageCustom) : super() {
uuid = icon.uuid
imageData = icon.imageData
}
constructor(parcel: Parcel) {
uuid = parcel.readSerializable() as UUID
// TODO Take too much memories
// parcel.readByteArray(imageData);
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(uuid)
// Too big for a parcelable dest.writeByteArray(imageData);
}
override fun hashCode(): Int {
@@ -64,6 +55,10 @@ class IconImageCustom : IconImage {
return result
}
override fun getIconImageToDraw(): IconImage {
return IconImage(this)
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
@@ -74,17 +69,10 @@ class IconImageCustom : IconImage {
return uuid == other.uuid
}
override val iconId: Int
get() = UNKNOWN_ID
override val isUnknown: Boolean
get() = this == UNKNOWN_ICON
override val isMetaStreamIcon: Boolean
get() = false
val isUnknown: Boolean
get() = uuid == DatabaseVersioned.UUID_ZERO
companion object {
val UNKNOWN_ICON = IconImageCustom(DatabaseVersioned.UUID_ZERO, ByteArray(0))
@JvmField
val CREATOR: Parcelable.Creator<IconImageCustom> = object : Parcelable.Creator<IconImageCustom> {

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
abstract class IconImageDraw {
var selected = false
/**
* Only to retrieve an icon image to Draw, to not use as object to manipulate
*/
abstract fun getIconImageToDraw(): IconImage
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2019 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
import org.apache.commons.collections.map.AbstractReferenceMap
import org.apache.commons.collections.map.ReferenceMap
import java.util.UUID
class IconImageFactory {
/** customIconMap
* Cache for icon drawable.
* Keys: Integer, Values: IconImageStandard
*/
private val cache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
/** standardIconMap
* Cache for icon drawable.
* Keys: UUID, Values: IconImageCustom
*/
private val customCache = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
val unknownIcon: IconImageStandard
get() = getIcon(IconImage.UNKNOWN_ID)
val keyIcon: IconImageStandard
get() = getIcon(IconImageStandard.KEY)
val trashIcon: IconImageStandard
get() = getIcon(IconImageStandard.TRASH)
val folderIcon: IconImageStandard
get() = getIcon(IconImageStandard.FOLDER)
fun getIcon(iconId: Int): IconImageStandard {
var icon: IconImageStandard? = cache[iconId] as IconImageStandard?
if (icon == null) {
icon = IconImageStandard(iconId)
cache[iconId] = icon
}
return icon
}
fun getIcon(iconUuid: UUID): IconImageCustom {
var icon: IconImageCustom? = customCache[iconUuid] as IconImageCustom?
if (icon == null) {
icon = IconImageCustom(iconUuid)
customCache[iconUuid] = icon
}
return icon
}
fun put(icon: IconImageCustom) {
customCache[icon.uuid] = icon
}
}

View File

@@ -21,36 +21,46 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
class IconImageStandard : IconImage {
class IconImageStandard : Parcelable, IconImageDraw {
val id: Int
constructor() {
this.iconId = KEY
this.id = KEY_ID
}
constructor(iconId: Int) {
this.iconId = iconId
}
constructor(icon: IconImageStandard) {
this.iconId = icon.iconId
if (!isCorrectIconId(iconId))
this.id = KEY_ID
else
this.id = iconId
}
constructor(parcel: Parcel) {
iconId = parcel.readInt()
id = parcel.readInt()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(iconId)
dest.writeInt(id)
}
override fun hashCode(): Int {
val prime = 31
var result = 1
result = prime * result + iconId
result = prime * result + id
return result
}
override fun getIconImageToDraw(): IconImage {
return IconImage(this)
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
@@ -59,22 +69,18 @@ class IconImageStandard : IconImage {
if (other !is IconImageStandard) {
return false
}
return iconId == other.iconId
return id == other.id
}
override val iconId: Int
override val isUnknown: Boolean
get() = iconId == UNKNOWN_ID
override val isMetaStreamIcon: Boolean
get() = iconId == 0
companion object {
const val KEY = 0
const val TRASH = 43
const val FOLDER = 48
const val KEY_ID = 0
const val TRASH_ID = 43
const val FOLDER_ID = 48
fun isCorrectIconId(iconId: Int): Boolean {
return iconId in 0 until NB_ICONS
}
@JvmField
val CREATOR: Parcelable.Creator<IconImageStandard> = object : Parcelable.Creator<IconImageStandard> {

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
import android.util.Log
import com.kunzisoft.keepass.database.element.database.BinaryByte
import com.kunzisoft.keepass.database.element.database.BinaryByte.Companion.MAX_BINARY_BYTES
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.database.BinaryFile
import com.kunzisoft.keepass.database.element.database.CustomIconPool
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
import java.io.File
import java.util.*
class IconsManager {
private val standardCache = List(NB_ICONS) {
IconImageStandard(it)
}
private val customCache = CustomIconPool()
fun getIcon(iconId: Int): IconImageStandard {
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
return standardCache[searchIconId]
}
fun doForEachStandardIcon(action: (IconImageStandard) -> Unit) {
standardCache.forEach { icon ->
action.invoke(icon)
}
}
/*
* Custom
*/
fun buildNewCustomIcon(cacheDirectory: File,
key: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) {
// Create a binary file for a brand new custom icon
addCustomIcon(cacheDirectory, key, -1, result)
}
fun addCustomIcon(cacheDirectory: File,
key: UUID? = null,
dataSize: Int,
result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = customCache.put(key) { uniqueBinaryId ->
// Create a byte array for better performance with small data
if (dataSize in 1..MAX_BINARY_BYTES) {
BinaryByte()
} else {
val fileInCache = File(cacheDirectory, uniqueBinaryId)
BinaryFile(fileInCache)
}
}
result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary)
}
fun getIcon(iconUuid: UUID): IconImageCustom {
return IconImageCustom(iconUuid)
}
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return customCache.isBinaryDuplicate(binaryData)
}
fun removeCustomIcon(iconUuid: UUID) {
val binary = customCache[iconUuid]
customCache.remove(iconUuid)
try {
binary?.clear()
} catch (e: Exception) {
Log.w(TAG, "Unable to remove custom icon binary", e)
}
}
fun getBinaryForCustomIcon(iconUuid: UUID): BinaryData? {
return customCache[iconUuid]
}
fun doForEachCustomIcon(action: (IconImageCustom, BinaryData) -> Unit) {
customCache.doForEachBinary { key, binary ->
action.invoke(IconImageCustom(key), binary)
}
}
/**
* Clear the cache of icons
*/
fun clearCache() {
try {
customCache.clear()
} catch(e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}
}
companion object {
private val TAG = IconsManager::class.java.name
}
}

View File

@@ -22,11 +22,10 @@ package com.kunzisoft.keepass.database.element.node
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import org.joda.time.LocalDateTime
/**
@@ -88,7 +87,7 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
final override var parent: Parent? = null
override var icon: IconImage = IconImageStandard()
final override var icon: IconImage = IconImage()
final override var creationTime: DateInstant = DateInstant()

View File

@@ -177,6 +177,7 @@ class DatabaseInputKDB(cacheDirectory: File)
val newRoot = mDatabase.createGroup()
newRoot.level = -1
mDatabase.rootGroup = newRoot
mDatabase.addGroupIndex(newRoot)
// Import all nodes
var newGroup: GroupKDB? = null
@@ -231,7 +232,7 @@ class DatabaseInputKDB(cacheDirectory: File)
if (iconId == -1) {
iconId = 0
}
entry.icon = mDatabase.iconFactory.getIcon(iconId)
entry.icon.standard = mDatabase.getStandardIcon(iconId)
}
}
0x0004 -> {
@@ -260,7 +261,7 @@ class DatabaseInputKDB(cacheDirectory: File)
}
0x0007 -> {
newGroup?.let { group ->
group.icon = mDatabase.iconFactory.getIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
group.icon.standard = mDatabase.getStandardIcon(cipherInputStream.readBytes4ToUInt().toKotlinInt())
} ?:
newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
@@ -305,7 +306,7 @@ class DatabaseInputKDB(cacheDirectory: File)
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory)
val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory)
entry.binaryData = binaryAttachment
val cipherKey = mDatabase.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to load binaries")

View File

@@ -29,14 +29,13 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -79,7 +78,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
private var ctxStringName: String? = null
private var ctxStringValue: ProtectedString? = null
private var ctxBinaryName: String? = null
private var ctxBinaryValue: BinaryAttachment? = null
private var ctxBinaryValue: BinaryData? = null
private var ctxATName: String? = null
private var ctxATSeq: String? = null
private var entryInHistory = false
@@ -277,7 +276,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
val byteLength = size - 1
// No compression at this level
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
val protectedBinary = mDatabase.buildNewAttachment(cacheDirectory, false, protectedFlag)
val cipherKey = mDatabase.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to load binaries")
protectedBinary.getOutputDataStream(cipherKey).use { outputStream ->
@@ -507,9 +506,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
} else if (name.equals(DatabaseKDBXXML.ElemNotes, ignoreCase = true)) {
ctxGroup?.notes = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxGroup?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
return switchContext(ctx, KdbContext.GroupTimes, xpp)
} else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) {
@@ -561,9 +560,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
KdbContext.Entry -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxEntry?.nodeId = NodeIdUUID(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon = mDatabase.iconFactory.getIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxEntry?.iconCustom = mDatabase.iconFactory.getIcon(readUuid(xpp))
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
ctxEntry?.foregroundColor = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
@@ -705,9 +704,13 @@ class DatabaseInputKDBX(cacheDirectory: File)
return KdbContext.Meta
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) {
val icon = IconImageCustom(customIconID, customIconData!!)
mDatabase.addCustomIcon(icon)
mDatabase.iconFactory.put(icon)
mDatabase.addCustomIcon(cacheDirectory, customIconID, customIconData!!.size) { _, binary ->
mDatabase.loadedCipherKey?.let { cipherKey ->
binary?.getOutputDataStream(cipherKey)?.use { outputStream ->
outputStream.write(customIconData)
}
}
}
}
customIconID = DatabaseVersioned.UUID_ZERO
@@ -963,7 +966,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
}
@Throws(XmlPullParserException::class, IOException::class)
private fun readBinary(xpp: XmlPullParser): BinaryAttachment? {
private fun readBinary(xpp: XmlPullParser): BinaryData? {
// Reference Id to a binary already present in binary pool
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
@@ -978,7 +981,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
var binaryRetrieve = mDatabase.binaryPool[id]
// Create empty binary if not retrieved in pool
if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
binaryRetrieve = mDatabase.buildNewAttachment(cacheDirectory,
compression = false, protection = false, binaryPoolId = id)
}
return binaryRetrieve
@@ -994,7 +997,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
}
@Throws(IOException::class, XmlPullParserException::class)
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryData? {
var compressed = false
var protected = true
@@ -1015,7 +1018,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
return null
// Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory, compressed, protected, binaryId)
val binaryCipherKey = mDatabase.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to load binaries")
try {

View File

@@ -216,10 +216,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
GroupOutputKDB(group, outputStream).output()
}
// Entries
val binaryCipherKey = mDatabaseKDB.loadedCipherKey
?: throw DatabaseOutputException("Unable to retrieve cipher key to write binaries")
mDatabaseKDB.doForEachEntryInIndex { entry ->
EntryOutputKDB(entry, outputStream, binaryCipherKey).output()
EntryOutputKDB(entry, outputStream, mDatabaseKDB.loadedCipherKey).output()
}
}

View File

@@ -35,7 +35,6 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BA
import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -137,29 +136,28 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
val binaryCipherKey = database.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to write binaries")
// Force decompression to add binary in header
protectedBinary.decompress(binaryCipherKey)
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
dataOutputStream.writeByte(flag)
database.loadedCipherKey?.let { binaryCipherKey ->
database.binaryPool.doForEachOrderedBinaryWithoutDuplication { _, binary ->
// Force decompression to add binary in header
binary.decompress(binaryCipherKey)
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (binary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes { buffer ->
dataOutputStream.write(buffer)
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes { buffer ->
dataOutputStream.write(buffer)
}
}
}
}
} ?: Log.e(TAG, "Unable to retrieve cipher key to write head binaries")
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
dataOutputStream.writeInt(0)
@@ -363,10 +361,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeUuid(DatabaseKDBXXML.ElemUuid, group.id)
writeObject(DatabaseKDBXXML.ElemName, group.title)
writeObject(DatabaseKDBXXML.ElemNotes, group.notes)
writeObject(DatabaseKDBXXML.ElemIcon, group.icon.iconId.toLong())
writeObject(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong())
if (group.iconCustom != IconImageCustom.UNKNOWN_ICON) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.iconCustom.uuid)
if (!group.icon.custom.isUnknown) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid)
}
writeTimes(group)
@@ -388,10 +386,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemEntry)
writeUuid(DatabaseKDBXXML.ElemUuid, entry.id)
writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.iconId.toLong())
writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong())
if (entry.iconCustom != IconImageCustom.UNKNOWN_ICON) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.iconCustom.uuid)
if (!entry.icon.custom.isUnknown) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid)
}
writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
@@ -496,30 +494,31 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
// With kdbx4, don't use this method because binaries are in header file
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
// Use indexes because necessarily (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
val binary = keyBinary.binary
if (binary.length > 0) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Write the XML
val binaryCipherKey = mDatabaseKDBX.loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to write binaries")
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
mDatabaseKDBX.loadedCipherKey?.let { binaryCipherKey ->
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
// Use indexes because necessarily (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinaryWithoutDuplication { index, binary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
if (binary.getSize() > 0) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
try {
// Write the XML
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to write binary", e)
}
}
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
} ?: Log.e(TAG, "Unable to retrieve cipher key to write binaries")
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
@@ -700,21 +699,39 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeCustomIconList() {
val customIcons = mDatabaseKDBX.customIcons
if (customIcons.size == 0) return
mDatabaseKDBX.loadedCipherKey?.let { cipherKey ->
var firstElement = true
mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary ->
if (binary.dataExists()) {
// Write the parent tag
if (firstElement) {
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
firstElement = false
}
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
for (icon in customIcons) {
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid)
var customImageData = ByteArray(0)
try {
binary.getInputDataStream(cipherKey).use { inputStream ->
customImageData = inputStream.readBytes()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to write custom icon", e)
} finally {
writeObject(DatabaseKDBXXML.ElemCustomIconItemData,
String(Base64.encode(customImageData, BASE_64_FLAG)))
}
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, icon.uuid)
writeObject(DatabaseKDBXXML.ElemCustomIconItemData, String(Base64.encode(icon.imageData, BASE_64_FLAG)))
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
}
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
}
}
// Close the parent tag
if (!firstElement) {
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
}
} ?: Log.e(TAG, "Unable to retrieve cipher key to write custom icons")
}
private fun safeXmlString(text: String): String {

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.database.file.output
import android.util.Log
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -34,7 +35,7 @@ import java.nio.charset.Charset
*/
class EntryOutputKDB(private val mEntry: EntryKDB,
private val mOutputStream: OutputStream,
private val mCipherKey: Database.LoadedKey) {
private val mCipherKey: Database.LoadedKey?) {
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
@Throws(DatabaseOutputException::class)
@@ -53,7 +54,7 @@ class EntryOutputKDB(private val mEntry: EntryKDB,
// Image ID
mOutputStream.write(IMAGEID_FIELD_TYPE)
mOutputStream.write(IMAGEID_FIELD_SIZE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.iconId)))
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mEntry.icon.standard.id)))
// Title
//byte[] title = mEntry.title.getBytes("UTF-8");
@@ -93,20 +94,22 @@ class EntryOutputKDB(private val mEntry: EntryKDB,
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription)
// Binary
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
val binaryData = mEntry.binaryData
val binaryDataLength = binaryData?.length ?: 0L
// Write data length
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
// Write data
if (binaryDataLength > 0) {
binaryData?.getInputDataStream(mCipherKey).use { inputStream ->
inputStream?.readAllBytes { buffer ->
mOutputStream.write(buffer)
mCipherKey?.let { cipherKey ->
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
val binaryData = mEntry.binaryData
val binaryDataLength = binaryData?.getSize() ?: 0L
// Write data length
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
// Write data
if (binaryDataLength > 0) {
binaryData?.getInputDataStream(cipherKey).use { inputStream ->
inputStream?.readAllBytes { buffer ->
mOutputStream.write(buffer)
}
inputStream?.close()
}
inputStream?.close()
}
}
} ?: Log.e(TAG, "Unable to retrieve cipher key to write entry binary")
// End
mOutputStream.write(END_FIELD_TYPE)
@@ -138,6 +141,8 @@ class EntryOutputKDB(private val mEntry: EntryKDB,
}
companion object {
private val TAG = EntryOutputKDB::class.java.name
// Constants
private val UUID_FIELD_TYPE:ByteArray = uShortTo2Bytes(1)
private val GROUPID_FIELD_TYPE:ByteArray = uShortTo2Bytes(2)

View File

@@ -72,7 +72,7 @@ class GroupOutputKDB(private val mGroup: GroupKDB,
// Image ID
mOutputStream.write(IMAGEID_FIELD_TYPE)
mOutputStream.write(IMAGEID_FIELD_SIZE)
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.iconId)))
mOutputStream.write(uIntTo4Bytes(UnsignedInt(mGroup.icon.standard.id)))
// Level
mOutputStream.write(LEVEL_FIELD_TYPE)

View File

@@ -81,6 +81,7 @@ class SearchHelper {
max: Int): Group? {
val searchGroup = database.createGroup()
searchGroup?.isVirtual = true
searchGroup?.title = "\"" + searchQuery + "\""
// Search all entries

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.education
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Color
import android.util.Log
import androidx.preference.PreferenceManager
import com.getkeepsafe.taptargetview.TapTarget
@@ -74,6 +75,24 @@ open class Education(val activity: Activity) {
editor.apply()
}
protected fun getCircleColor(): Int {
val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationCircleColor))
val colorControl = typedArray.getColor(0, Color.GREEN)
typedArray.recycle()
return colorControl
}
protected fun getCircleAlpha(): Float {
return 0.98F
}
protected fun getTextColor(): Int {
val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.educationTextColor))
val colorControl = typedArray.getColor(0, Color.WHITE)
typedArray.recycle()
return colorControl
}
companion object {
private const val EDUCATION_PREFERENCE = "kdbxeducation"

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education
import android.app.Activity
import android.graphics.Color
import android.view.View
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
@@ -38,7 +37,9 @@ class EntryActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_field_copy_title),
activity.getString(R.string.education_field_copy_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(false)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -68,7 +69,9 @@ class EntryActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_entry_edit_title),
activity.getString(R.string.education_entry_edit_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education
import android.app.Activity
import android.graphics.Color
import android.view.View
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
@@ -40,7 +39,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_generate_password_title),
activity.getString(R.string.education_generate_password_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -69,7 +70,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_entry_new_field_title),
activity.getString(R.string.education_entry_new_field_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -98,7 +101,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_add_attachment_title),
activity.getString(R.string.education_add_attachment_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -127,7 +132,9 @@ class EntryEditActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_setup_OTP_title),
activity.getString(R.string.education_setup_OTP_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.education
import android.app.Activity
import android.graphics.Color
import androidx.core.content.ContextCompat
import android.view.View
import androidx.core.content.ContextCompat
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
@@ -43,8 +43,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_create_database_title),
activity.getString(R.string.education_create_database_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_database_plus_white_24dp))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -73,8 +75,10 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_select_database_title),
activity.getString(R.string.education_select_database_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_folder_white_24dp))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education
import android.app.Activity
import android.graphics.Color
import android.view.View
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
@@ -36,7 +35,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_new_node_title),
activity.getString(R.string.education_new_node_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(false)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -61,7 +62,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_search_title),
activity.getString(R.string.education_search_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -86,7 +89,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_sort_title),
activity.getString(R.string.education_sort_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -111,7 +116,9 @@ class GroupActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_lock_title),
activity.getString(R.string.education_lock_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.education
import android.app.Activity
import android.graphics.Color
import androidx.core.content.ContextCompat
import android.view.View
import com.getkeepsafe.taptargetview.TapTarget
@@ -37,8 +36,10 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_unlock_title),
activity.getString(R.string.education_unlock_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round))
.textColorInt(Color.WHITE)
.textColorInt(getTextColor())
.tintTarget(false)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -63,7 +64,9 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_read_only_title),
activity.getString(R.string.education_read_only_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
@@ -88,7 +91,9 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_advanced_unlock_title),
activity.getString(R.string.education_advanced_unlock_summary))
.textColorInt(Color.WHITE)
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(false)
.cancelable(true),
object : TapTargetView.Listener() {

View File

@@ -26,111 +26,237 @@ import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Log
import android.widget.ImageView
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import org.apache.commons.collections.map.AbstractReferenceMap
import org.apache.commons.collections.map.ReferenceMap
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.HashMap
/**
* Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint
*/
class IconDrawableFactory {
class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedKey?,
private val retrieveCustomIconBinary : (iconId: UUID) -> BinaryData?) {
/** customIconMap
* Cache for icon drawable.
* Keys: UUID, Values: Drawables
*/
private val customIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
private val customIconMap = HashMap<UUID, WeakReference<Drawable>>()
/** standardIconMap
* Cache for icon drawable.
* Keys: Integer, Values: Drawables
*/
private val standardIconMap = ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
private val standardIconMap = HashMap<CacheKey, WeakReference<Drawable>>()
/**
* Utility method to assign a drawable to an ImageView and tint it
* Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed
*/
fun assignDrawableToImageView(superDrawable: SuperDrawable, imageView: ImageView?, tint: Boolean, tintColor: Int) {
if (imageView != null) {
imageView.setImageDrawable(superDrawable.drawable)
if (superDrawable.tintable && tint) {
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable {
val icon = iconDraw.getIconImageToDraw()
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
if (customIconBinary != null && customIconBinary.dataExists()) {
getIconDrawable(context.resources, icon.custom, customIconBinary)?.let {
return SuperDrawable(it)
}
}
val iconPack = IconPackChooser.getSelectedIconPack(context)
iconPack?.iconToResId(icon.standard.id)?.let { iconId ->
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable())
} ?: run {
return SuperDrawable(PatternIcon(context.resources).blankDrawable)
}
}
/**
* Build a custom [Drawable] from custom [icon]
*/
private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? {
val patternIcon = PatternIcon(resources)
val cipherKey = retrieveCipherKey()
if (cipherKey != null) {
val draw: Drawable? = customIconMap[icon.uuid]?.get()
if (draw == null) {
iconCustomBinary?.let { binaryFile ->
try {
var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(cipherKey))
bitmap?.let { bitmapIcon ->
bitmap = resize(bitmapIcon, patternIcon)
val createdDraw = BitmapDrawable(resources, bitmap)
customIconMap[icon.uuid] = WeakReference(createdDraw)
return createdDraw
}
} catch (e: Exception) {
customIconMap.remove(icon.uuid)
Log.e(TAG, "Unable to create the bitmap icon", e)
}
}
} else {
ImageViewCompat.setImageTintList(imageView, null)
return draw
}
}
return null
}
/**
* Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet)
* , then tint it with [tintColor] if needed
*/
private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tintColor: Int): Drawable {
val newCacheKey = CacheKey(iconId, width, true, tintColor)
var draw: Drawable? = standardIconMap[newCacheKey]?.get()
if (draw == null) {
try {
draw = ResourcesCompat.getDrawable(resources, iconId, null)
} catch (e: Exception) {
Log.e(TAG, "Can't get icon", e)
}
if (draw != null) {
standardIconMap[newCacheKey] = WeakReference(draw)
}
}
if (draw == null) {
draw = PatternIcon(resources).blankDrawable
}
draw.isFilterBitmap = false
return draw
}
/**
* Resize the custom icon to match the built in icons
*
* @param bitmap Bitmap to resize
* @return Bitmap resized
*/
private fun resize(bitmap: Bitmap, dimensionPattern: PatternIcon): Bitmap {
val width = bitmap.width
val height = bitmap.height
return if (width == dimensionPattern.width && height == dimensionPattern.height) {
bitmap
} else Bitmap.createScaledBitmap(bitmap, dimensionPattern.width, dimensionPattern.height, true)
}
/**
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
*/
fun assignDatabaseIcon(imageView: ImageView,
icon: IconImageDraw,
tintColor: Int = Color.WHITE) {
try {
val context = imageView.context
CoroutineScope(Dispatchers.IO).launch {
addToCustomCache(context.resources, icon)
withContext(Dispatchers.Main) {
val superDrawable = getIconSuperDrawable(context,
icon,
imageView.width,
tintColor)
imageView.setImageDrawable(superDrawable.drawable)
if (superDrawable.tintable) {
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
} else {
ImageViewCompat.setImageTintList(imageView, null)
}
}
}
} catch (e: Exception) {
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
}
}
/**
* Utility method to assign a drawable to a RemoteView and tint it
* Build a bitmap from a database [icon]
*/
fun assignDrawableToRemoteViews(superDrawable: SuperDrawable,
remoteViews: RemoteViews,
imageId: Int,
tintColor: Int = Color.BLACK) {
val bitmap = superDrawable.drawable.toBitmap()
// Tint bitmap if it's not a custom icon
if (superDrawable.tintable && bitmap.isMutable) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
fun getBitmapFromIcon(context: Context,
icon: IconImageDraw,
tintColor: Int = Color.BLACK): Bitmap? {
try {
val superDrawable = getIconSuperDrawable(context,
icon,
24,
tintColor)
val bitmap = superDrawable.drawable.toBitmap()
// Tint bitmap if it's not a custom icon
if (superDrawable.tintable && bitmap.isMutable) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
return bitmap
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to create bitmap from icon", e)
}
remoteViews.setImageViewBitmap(imageId, bitmap)
return null
}
/**
* Utility method to assign a drawable to a icon and tint it
* Simple method to init the cache with the custom icon and be much faster next time
*/
@RequiresApi(Build.VERSION_CODES.M)
fun assignDrawableToIcon(superDrawable: SuperDrawable,
tintColor: Int = Color.BLACK): Icon {
val bitmap = superDrawable.drawable.toBitmap()
// Tint bitmap if it's not a custom icon
if (superDrawable.tintable && bitmap.isMutable) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
return Icon.createWithBitmap(bitmap)
private fun addToCustomCache(resources: Resources, iconDraw: IconImageDraw) {
val icon = iconDraw.getIconImageToDraw()
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
if (customIconBinary != null
&& customIconBinary.dataExists()
&& !customIconMap.containsKey(icon.custom.uuid))
getIconDrawable(resources, icon.custom, customIconBinary)
}
/**
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
* Clear a specific icon from the cache
*/
fun getIconSuperDrawable(context: Context, icon: IconImage, width: Int, tint: Boolean = false, tintColor: Int = Color.WHITE): SuperDrawable {
return when (icon) {
is IconImageStandard -> {
val resId = IconPackChooser.getSelectedIconPack(context)?.iconToResId(icon.iconId) ?: R.drawable.ic_blank_32dp
getIconSuperDrawable(context, resId, width, tint, tintColor)
}
is IconImageCustom -> {
SuperDrawable(getIconDrawable(context.resources, icon))
}
else -> {
SuperDrawable(PatternIcon(context.resources).blankDrawable)
}
fun clearFromCache(icon: IconImageCustom) {
customIconMap.remove(icon.uuid)
}
/**
* Clear the cache of icons
*/
fun clearCache() {
standardIconMap.clear()
customIconMap.clear()
}
/**
* Build a blankDrawable drawable
* @param res Resource to build the drawable
*/
private class PatternIcon(res: Resources) {
var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)
var width = -1
var height = -1
init {
width = res.getDimension(R.dimen.icon_size).toInt()
height = res.getDimension(R.dimen.icon_size).toInt()
blankDrawable.setBounds(0, 0, width, height)
}
}
/**
* Get the [SuperDrawable] IconImageStandard from [iconId] (cache, or build it and add it to the cache if not exists yet)
* , then [tint] it with [tintColor] if needed
* Utility class to prevent a custom icon to be tint
*/
fun getIconSuperDrawable(context: Context, iconId: Int, width: Int, tint: Boolean, tintColor: Int): SuperDrawable {
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tint, tintColor), true)
}
class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false)
/**
* Key class to retrieve a Drawable in the cache if it's tinted or not
@@ -161,189 +287,9 @@ class IconDrawableFactory {
}
}
/**
* Build a custom [Drawable] from custom [icon]
*/
private fun getIconDrawable(resources: Resources, icon: IconImageCustom): Drawable {
val patternIcon = PatternIcon(resources)
var draw: Drawable? = customIconMap[icon.uuid] as Drawable?
if (draw == null) {
var bitmap: Bitmap? = BitmapFactory.decodeByteArray(icon.imageData, 0, icon.imageData.size)
// Could not understand custom icon
bitmap?.let { bitmapIcon ->
bitmap = resize(bitmapIcon, patternIcon)
draw = BitmapDrawable(resources, bitmap)
customIconMap[icon.uuid] = draw
return draw!!
}
} else {
return draw!!
}
return patternIcon.blankDrawable
}
/**
* Get the standard [Drawable] icon from [iconId] (cache or build it and add it to the cache if not exists yet)
* , then [tint] it with [tintColor] if needed
*/
private fun getIconDrawable(resources: Resources, iconId: Int, width: Int, tint: Boolean, tintColor: Int): Drawable {
val newCacheKey = CacheKey(iconId, width, tint, tintColor)
var draw: Drawable? = standardIconMap[newCacheKey] as Drawable?
if (draw == null) {
try {
draw = ResourcesCompat.getDrawable(resources, iconId, null)
} catch (e: Exception) {
Log.e(TAG, "Can't get icon", e)
}
if (draw != null) {
standardIconMap[newCacheKey] = draw
}
}
if (draw == null) {
draw = PatternIcon(resources).blankDrawable
}
draw.isFilterBitmap = false
return draw
}
/**
* Resize the custom icon to match the built in icons
*
* @param bitmap Bitmap to resize
* @return Bitmap resized
*/
private fun resize(bitmap: Bitmap, dimensionPattern: PatternIcon): Bitmap {
val width = bitmap.width
val height = bitmap.height
return if (width == dimensionPattern.width && height == dimensionPattern.height) {
bitmap
} else Bitmap.createScaledBitmap(bitmap, dimensionPattern.width, dimensionPattern.height, true)
}
/**
* Clear the cache of icons
*/
fun clearCache() {
standardIconMap.clear()
customIconMap.clear()
}
private class PatternIcon
/**
* Build a blankDrawable drawable
* @param res Resource to build the drawable
*/(res: Resources) {
var blankDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)
var width = -1
var height = -1
init {
width = res.getDimension(R.dimen.icon_size).toInt()
height = res.getDimension(R.dimen.icon_size).toInt()
blankDrawable.setBounds(0, 0, width, height)
}
}
/**
* Utility class to prevent a custom icon to be tint
*/
class SuperDrawable(var drawable: Drawable, var tintable: Boolean = false)
companion object {
private val TAG = IconDrawableFactory::class.java.name
}
}
/**
* Assign a default database icon to an ImageView and tint it with [tintColor] if needed
*/
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory,
tintColor: Int = Color.WHITE) {
try {
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
iconFactory.assignDrawableToImageView(
iconFactory.getIconSuperDrawable(context,
selectedIconPack.defaultIconId,
width,
selectedIconPack.tintable(),
tintColor),
this,
selectedIconPack.tintable(),
tintColor)
}
} catch (e: Exception) {
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
}
}
/**
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
*/
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.WHITE) {
try {
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
iconFactory.assignDrawableToImageView(
iconFactory.getIconSuperDrawable(context,
icon,
width,
true,
tintColor),
this,
selectedIconPack.tintable(),
tintColor)
}
} catch (e: Exception) {
Log.e(ImageView::class.java.name, "Unable to assign icon in image view", e)
}
}
fun RemoteViews.assignDatabaseIcon(context: Context,
imageId: Int,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK) {
try {
iconFactory.assignDrawableToRemoteViews(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
this,
imageId,
tintColor)
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun createIconFromDatabaseIcon(context: Context,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK): Icon? {
try {
return iconFactory.assignDrawableToIcon(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
tintColor)
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}

View File

@@ -78,7 +78,7 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) {
// Build the list of icons
var num = 0
while (num <= NB_ICONS) {
while (num < NB_ICONS) {
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
val resId = resources.getIdentifier(
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
@@ -134,7 +134,6 @@ class IconPack(packageName: String, resources: Resources, resourceId: Int) {
}
companion object {
private const val NB_ICONS = 68
const val NB_ICONS = 69
}
}

View File

@@ -93,7 +93,7 @@ object IconPackChooser {
fun setSelectedIconPack(iconPackIdString: String?) {
for (iconPack in iconPackList) {
if (iconPack.id == iconPackIdString) {
Database.getInstance().drawFactory.clearCache()
Database.getInstance().iconDrawableFactory.clearCache()
iconPackSelected = iconPack
break
}

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryByte
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
@@ -33,7 +33,7 @@ data class EntryAttachmentState(var attachment: Attachment,
var previewState: AttachmentState = AttachmentState.NULL) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryByte()),
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
parcel.readInt(),

View File

@@ -3,14 +3,14 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
class GroupInfo : NodeInfo {
var notes: String? = null
init {
icon = IconImageStandard(FOLDER)
icon.standard = IconImageStandard(FOLDER_ID)
}
constructor(): super()

View File

@@ -4,12 +4,11 @@ import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
open class NodeInfo() : Parcelable {
var title: String = ""
var icon: IconImage = IconImageStandard()
var icon: IconImage = IconImage()
var creationTime: DateInstant = DateInstant()
var lastModificationTime: DateInstant = DateInstant()
var expires: Boolean = false

View File

@@ -321,11 +321,9 @@ object OtpEntryFields {
return try {
// KeeOtp string format
val query = breakDownKeyValuePairs(plainText)
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.type = OtpType.TOTP
true
} catch (exception: Exception) {
@@ -413,7 +411,7 @@ object OtpEntryFields {
val output = HashMap<String, String>()
for (element in elements) {
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
output[pair[0]] = pair[1]
output[pair[0].toLowerCase(Locale.ENGLISH)] = pair[1]
}
return output
}

View File

@@ -29,13 +29,10 @@ import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import kotlinx.coroutines.*
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
@@ -86,7 +83,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
}
override fun onBind(intent: Intent): IBinder? {
override fun onBind(intent: Intent): IBinder {
return mActionTaskBinder
}
@@ -347,24 +344,27 @@ class AttachmentFileNotificationService: LockNotificationService() {
when (streamDirection) {
StreamDirection.UPLOAD -> {
uploadToDatabase(
BinaryDatabaseManager.uploadToDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
contentResolver, 1024,
attachment.binaryData,
contentResolver,
{ percent ->
publishProgress(percent)
},
{ // Cancellation
downloadState == AttachmentState.CANCELED
}
) { percent ->
publishProgress(percent)
}
)
}
StreamDirection.DOWNLOAD -> {
downloadFromDatabase(
BinaryDatabaseManager.downloadFromDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
contentResolver, 1024) { percent ->
publishProgress(percent)
}
attachment.binaryData,
contentResolver,
{ percent ->
publishProgress(percent)
}
)
}
}
} catch (e: Exception) {
@@ -396,57 +396,6 @@ class AttachmentFileNotificationService: LockNotificationService() {
attachmentNotification.entryAttachmentState.downloadState = AttachmentState.CANCELED
}
fun downloadFromDatabase(attachmentToUploadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0L
val fileSize = binaryAttachment.length
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
binaryAttachment.getUnGzipInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes(bufferSize) { buffer ->
outputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
}
}
}
}
fun uploadToDatabase(attachmentFromDownloadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
canceled: ()-> Boolean = { false },
update: ((percent: Int)->Unit)? = null) {
var dataUploaded = 0L
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream ->
Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
binaryAttachment.getGzipOutputDataStream(binaryCipherKey).use { outputStream ->
inputStream.readAllBytes(bufferSize, canceled) { buffer ->
outputStream.write(buffer)
dataUploaded += buffer.size
try {
val percentDownload = (100 * dataUploaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
}
}
}
}
private fun publishProgress(percent: Int) {
// Publish progress
val currentTime = System.currentTimeMillis()

View File

@@ -138,12 +138,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val oldDatabaseModification = previousDatabaseInfo?.lastModification
val newDatabaseModification = lastFileDatabaseInfo.lastModification
val oldDatabaseSize = previousDatabaseInfo?.size
val conditionExists = previousDatabaseInfo != null
&& previousDatabaseInfo.exists != lastFileDatabaseInfo.exists
// To prevent dialog opening too often
// Add 10 seconds delta time to prevent spamming
val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null
val conditionLastModification =
(oldDatabaseModification != null && newDatabaseModification != null
&& oldDatabaseSize != null
&& oldDatabaseModification > 0 && newDatabaseModification > 0
&& oldDatabaseSize > 0
&& oldDatabaseModification < newDatabaseModification
&& mLastLocalSaveTime + 10000 < newDatabaseModification)

View File

@@ -385,7 +385,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
if (styleEnabled) {
Stylish.assignStyle(styleIdString)
Stylish.assignStyle(activity, styleIdString)
// Relaunch the current activity to redraw theme
(activity as? SettingsActivity?)?.apply {
keepCurrentScreen()
@@ -397,6 +397,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
styleEnabled
}
findPreference<ListPreference>(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ ->
(activity as? SettingsActivity?)?.apply {
keepCurrentScreen()
startActivity(intent)
finish()
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
true
}
findPreference<IconPackListPreference>(getString(R.string.setting_icon_pack_choose_key))?.setOnPreferenceChangeListener { _, newValue ->
var iconPackEnabled = true
val iconPackId = newValue as String

View File

@@ -26,6 +26,7 @@ import android.net.Uri
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -132,6 +133,21 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.show_uuid_default))
}
fun getStyle(context: Context): String {
val stylishPrefKey = context.getString(R.string.setting_style_key)
val defaultStyleString = context.getString(R.string.list_style_name_light)
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
.getString(stylishPrefKey, defaultStyleString)
?: defaultStyleString
return Stylish.retrieveEquivalentLightStyle(context, styleString)
}
fun getStyleBrightness(context: Context): String? {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.setting_style_brightness_key),
context.resources.getString(R.string.list_style_brightness_follow_system))
}
/**
* Retrieve the text size in % (1 for 100%)
*/

View File

@@ -0,0 +1,193 @@
package com.kunzisoft.keepass.tasks
import android.content.ContentResolver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryData
import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import kotlin.math.ceil
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.pow
object BinaryDatabaseManager {
fun downloadFromDatabase(attachmentToUploadUri: Uri,
binaryData: BinaryData,
contentResolver: ContentResolver,
update: ((percent: Int)->Unit)? = null,
canceled: ()-> Boolean = { false },
bufferSize: Int = DEFAULT_BUFFER_SIZE) {
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
downloadFromDatabase(outputStream, binaryData, update, canceled, bufferSize)
}
}
private fun downloadFromDatabase(outputStream: OutputStream,
binaryData: BinaryData,
update: ((percent: Int)->Unit)? = null,
canceled: ()-> Boolean = { false },
bufferSize: Int = DEFAULT_BUFFER_SIZE) {
val fileSize = binaryData.getSize()
var dataDownloaded = 0L
Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
binaryData.getUnGzipInputDataStream(binaryCipherKey).use { inputStream ->
inputStream.readAllBytes(bufferSize, canceled) { buffer ->
outputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.w(TAG, "Unable to call update callback during download", e)
}
}
}
}
}
fun uploadToDatabase(attachmentFromDownloadUri: Uri,
binaryData: BinaryData,
contentResolver: ContentResolver,
update: ((percent: Int)->Unit)? = null,
canceled: ()-> Boolean = { false },
bufferSize: Int = DEFAULT_BUFFER_SIZE) {
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.use { inputStream ->
uploadToDatabase(inputStream, fileSize, binaryData, update, canceled, bufferSize)
}
}
private fun uploadToDatabase(inputStream: InputStream,
fileSize: Long,
binaryData: BinaryData,
update: ((percent: Int)->Unit)? = null,
canceled: ()-> Boolean = { false },
bufferSize: Int = DEFAULT_BUFFER_SIZE) {
var dataUploaded = 0L
Database.getInstance().loadedCipherKey?.let { binaryCipherKey ->
binaryData.getGzipOutputDataStream(binaryCipherKey).use { outputStream ->
inputStream.readAllBytes(bufferSize, canceled) { buffer ->
outputStream.write(buffer)
dataUploaded += buffer.size
try {
val percentDownload = (100 * dataUploaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.w(TAG, "Unable to call update callback during upload", e)
}
}
}
}
}
fun resizeBitmapAndStoreDataInBinaryFile(contentResolver: ContentResolver,
bitmapUri: Uri?,
binaryData: BinaryData?) {
try {
binaryData?.let {
UriUtil.getUriInputStream(contentResolver, bitmapUri)?.use { inputStream ->
BitmapFactory.decodeStream(inputStream)?.let { bitmap ->
val bitmapResized = bitmap.resize(DEFAULT_ICON_WIDTH)
val byteArrayOutputStream = ByteArrayOutputStream()
bitmapResized?.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
val byteArrayInputStream = ByteArrayInputStream(bitmapData)
uploadToDatabase(
byteArrayInputStream,
bitmapData.size.toLong(),
binaryData
)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to resize bitmap to store it in binary", e)
}
}
/**
* reduces the size of the image
* @param image
* @param maxSize
* @return
*/
private fun Bitmap.resize(maxSize: Int): Bitmap? {
var width = this.width
var height = this.height
val bitmapRatio = width.toFloat() / height.toFloat()
if (bitmapRatio > 1) {
width = maxSize
height = (width / bitmapRatio).toInt()
} else {
height = maxSize
width = (height * bitmapRatio).toInt()
}
return Bitmap.createScaledBitmap(this, width, height, true)
}
fun loadBitmap(binaryData: BinaryData,
binaryCipherKey: Database.LoadedKey?,
maxWidth: Int,
actionOnFinish: (Bitmap?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<Bitmap?> = async {
runCatching {
binaryCipherKey?.let { binaryKey ->
val bitmap: Bitmap? = decodeSampledBitmap(binaryData,
binaryKey,
maxWidth)
bitmap
}
}.getOrNull()
}
withContext(Dispatchers.Main) {
actionOnFinish(asyncResult.await())
}
}
}
}
private fun decodeSampledBitmap(binaryData: BinaryData,
binaryCipherKey: Database.LoadedKey,
maxWidth: Int): Bitmap? {
// First decode with inJustDecodeBounds=true to check dimensions
return BitmapFactory.Options().run {
try {
inJustDecodeBounds = true
binaryData.getUnGzipInputDataStream(binaryCipherKey).use {
BitmapFactory.decodeStream(it, null, this)
}
// Calculate inSampleSize
var scale = 1
if (outHeight > maxWidth || outWidth > maxWidth) {
scale = 2.0.pow(ceil(ln(maxWidth / max(outHeight, outWidth).toDouble()) / ln(0.5))).toInt()
}
inSampleSize = scale
// Decode bitmap with inSampleSize set
inJustDecodeBounds = false
binaryData.getUnGzipInputDataStream(binaryCipherKey).use {
BitmapFactory.decodeStream(it, null, this)
}
} catch (e: Exception) {
null
}
}
}
private const val DEFAULT_ICON_WIDTH = 64
private val TAG = BinaryDatabaseManager::class.java.name
}

View File

@@ -42,8 +42,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
: RelativeLayout(context, attrs, defStyle) {
var addButtonView: FloatingActionButton? = null
private var addEntryView: View? = null
private var addGroupView: View? = null
private lateinit var addEntryView: View
private lateinit var addGroupView: View
private var addEntryEnable: Boolean = false
private var addGroupEnable: Boolean = false
@@ -82,8 +82,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
animationDuration = 300L
viewButtonMenuAnimation = AddButtonAnimation(addButtonView)
viewMenuAnimationAddEntry = ViewMenuAnimation(addEntryView, 0L, 150L)
viewMenuAnimationAddGroup = ViewMenuAnimation(addGroupView, 150L, 0L)
viewMenuAnimationAddEntry = ViewMenuAnimation(addEntryView, 150L, 0L)
viewMenuAnimationAddGroup = ViewMenuAnimation(addGroupView, 0L, 150L)
allowAction = true
state = State.CLOSE
@@ -111,8 +111,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
val viewEntryRect = Rect()
val viewGroupRect = Rect()
addButtonView?.getGlobalVisibleRect(viewButtonRect)
addEntryView?.getGlobalVisibleRect(viewEntryRect)
addGroupView?.getGlobalVisibleRect(viewGroupRect)
addEntryView.getGlobalVisibleRect(viewEntryRect)
addGroupView.getGlobalVisibleRect(viewGroupRect)
if (!(viewButtonRect.contains(event.rawX.toInt(), event.rawY.toInt())
&& viewEntryRect.contains(event.rawX.toInt(), event.rawY.toInt())
&& viewGroupRect.contains(event.rawX.toInt(), event.rawY.toInt()))) {
@@ -165,8 +165,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
*/
fun enableAddEntry(enable: Boolean) {
this.addEntryEnable = enable
if (enable && addEntryView != null && addEntryView!!.visibility != View.VISIBLE)
addEntryView!!.visibility = View.INVISIBLE
if (enable && addEntryView.visibility != View.VISIBLE)
addEntryView.visibility = View.INVISIBLE
disableViewIfNoAddAvailable()
}
@@ -176,13 +176,13 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
*/
fun enableAddGroup(enable: Boolean) {
this.addGroupEnable = enable
if (enable && addGroupView != null && addGroupView!!.visibility != View.VISIBLE)
addGroupView?.visibility = View.INVISIBLE
if (enable && addGroupView.visibility != View.VISIBLE)
addGroupView.visibility = View.INVISIBLE
disableViewIfNoAddAvailable()
}
private fun disableViewIfNoAddAvailable() {
visibility = if (!addEntryEnable || !addGroupEnable) {
visibility = if (!addEntryEnable && !addGroupEnable) {
View.GONE
} else {
View.VISIBLE
@@ -191,7 +191,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
fun setAddGroupClickListener(onClickListener: OnClickListener) {
if (addGroupEnable)
addGroupView?.setOnClickListener { view ->
addGroupView.setOnClickListener { view ->
onClickListener.onClick(view)
closeButtonIfOpen()
}
@@ -199,11 +199,11 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
fun setAddEntryClickListener(onClickListener: OnClickListener) {
if (addEntryEnable) {
addEntryView?.setOnClickListener { view ->
addEntryView.setOnClickListener { view ->
onClickListener.onClick(view)
closeButtonIfOpen()
}
addEntryView?.setOnClickListener { view ->
addEntryView.setOnClickListener { view ->
onClickListener.onClick(view)
closeButtonIfOpen()
}
@@ -248,7 +248,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
override fun onAnimationCancel(view: View) {}
internal fun startAnimation() {
fun startAnimation() {
view?.let { view ->
if (!isRotate) {
ViewCompat.animate(view)
@@ -298,7 +298,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
override fun onAnimationCancel(view: View) {}
internal fun startAnimation() {
fun startAnimation() {
view?.let { view ->
if (view.visibility == View.VISIBLE) {
// In

View File

@@ -20,9 +20,14 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import androidx.annotation.ColorInt
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
class SpecialModeView @JvmOverloads constructor(context: Context,
@@ -31,7 +36,13 @@ class SpecialModeView @JvmOverloads constructor(context: Context,
: Toolbar(context, attrs, defStyle) {
init {
setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_white_24dp)?.let { closeDrawable ->
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorControlNormal, typedValue, true)
@ColorInt val colorControl = typedValue.data
closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP)
navigationIcon = closeDrawable
}
title = resources.getString(R.string.selection_mode)
}

View File

@@ -19,19 +19,25 @@
*/
package com.kunzisoft.keepass.view
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.annotation.ColorInt
import androidx.appcompat.view.ActionMode
import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
class ToolbarAction @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = R.attr.actionToolbarAppearance)
defStyle: Int = androidx.appcompat.R.attr.toolbarStyle)
: Toolbar(context, attrs, defStyle) {
private var mActionModeCallback: ActionMode.Callback? = null
@@ -39,7 +45,13 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
private var isOpen = false
init {
setNavigationIcon(R.drawable.ic_close_white_24dp)
ContextCompat.getDrawable(context, R.drawable.ic_close_white_24dp)?.let { closeDrawable ->
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorControlNormal, typedValue, true)
@ColorInt val colorControl = typedValue.data
closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP)
navigationIcon = closeDrawable
}
}
fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode {
@@ -106,6 +118,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
override fun setCustomView(view: View?) {}
@SuppressLint("RestrictedApi")
override fun getMenuInflater(): MenuInflater {
return SupportMenuInflater(toolbarAction.context)
}

View File

@@ -0,0 +1,89 @@
package com.kunzisoft.keepass.viewmodels
import android.os.Parcel
import android.os.Parcelable
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
class IconPickerViewModel: ViewModel() {
val standardIconPicked: MutableLiveData<IconImageStandard> by lazy {
MutableLiveData<IconImageStandard>()
}
val customIconPicked: MutableLiveData<IconImageCustom> by lazy {
MutableLiveData<IconImageCustom>()
}
val customIconsSelected: MutableLiveData<List<IconImageCustom>> by lazy {
MutableLiveData<List<IconImageCustom>>()
}
val customIconAdded: MutableLiveData<IconCustomState> by lazy {
MutableLiveData<IconCustomState>()
}
val customIconRemoved: MutableLiveData<IconCustomState> by lazy {
MutableLiveData<IconCustomState>()
}
fun pickStandardIcon(icon: IconImageStandard) {
standardIconPicked.value = icon
}
fun pickCustomIcon(icon: IconImageCustom) {
customIconPicked.value = icon
}
fun selectCustomIcons(icons: List<IconImageCustom>) {
customIconsSelected.value = icons
}
fun deselectAllCustomIcons() {
customIconsSelected.value = listOf()
}
fun addCustomIcon(customIcon: IconCustomState) {
customIconAdded.value = customIcon
}
fun removeCustomIcon(customIcon: IconCustomState) {
customIconRemoved.value = customIcon
}
data class IconCustomState(var iconCustom: IconImageCustom? = null,
var error: Boolean = true,
var errorStringId: Int = -1,
var errorConsumed: Boolean = false): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(IconImageCustom::class.java.classLoader),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readByte() != 0.toByte())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(iconCustom, flags)
parcel.writeByte(if (error) 1 else 0)
parcel.writeInt(errorStringId)
parcel.writeByte(if (errorConsumed) 1 else 0)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<IconCustomState> {
override fun createFromParcel(parcel: Parcel): IconCustomState {
return IconCustomState(parcel)
}
override fun newArray(size: Int): Array<IconCustomState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccentLight" android:state_pressed="true" />
<item android:color="@color/grey" android:state_activated="true" />
<item android:color="@color/grey_dark" android:state_enabled="false" />
<item android:color="@color/white_grey" android:state_activated="true" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?attr/colorAccent" android:state_enabled="true" />
</selector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimaryDark" android:state_pressed="true" />
<item android:color="@color/grey_dark" android:state_enabled="false" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?attr/colorPrimary" android:state_enabled="true" />
</selector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/grey" android:state_activated="true" />
<item android:color="@color/grey_dark" android:state_enabled="false" />
<item android:color="@color/white_grey" android:state_activated="true" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?android:attr/textColorHintInverse" android:state_enabled="true" />
</selector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccentLight" android:state_pressed="true" />
<item android:color="@color/grey_dark" android:state_enabled="false" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?attr/colorAccent" android:state_enabled="true" />
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_selected="true" android:color="@color/colorTextInverse"/>
<item android:state_selected="true" android:color="@color/white"/>
<item android:color="?android:attr/textColorSecondary"/>
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_selected="true" android:color="@color/colorTextInverse"/>
<item android:state_selected="true" android:color="@color/white"/>
<item android:color="?android:attr/textColor"/>
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_selected="true" android:color="@color/colorTextInverse"/>
<item android:state_selected="true" android:color="@color/white"/>
<item android:color="?android:attr/textColorSecondary"/>
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_selected="true" android:color="@color/colorTextInverse"/>
<item android:state_selected="true" android:color="@color/white"/>
<item android:color="?android:attr/textColorPrimary"/>
</selector>

Some files were not shown because too many files have changed in this diff Show More