mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'release/2.9.14'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/*
|
||||
------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -81,6 +81,7 @@ class SearchHelper {
|
||||
max: Int): Group? {
|
||||
|
||||
val searchGroup = database.createGroup()
|
||||
searchGroup?.isVirtual = true
|
||||
searchGroup?.title = "\"" + searchQuery + "\""
|
||||
|
||||
// Search all entries
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%)
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user