diff --git a/app-icon-loader/.gitignore b/app-icon-loader/.gitignore
new file mode 100644
index 000000000..04d6aa0d6
--- /dev/null
+++ b/app-icon-loader/.gitignore
@@ -0,0 +1,2 @@
+.cxx
+.externalNativeBuild
diff --git a/app-icon-loader/build.gradle b/app-icon-loader/build.gradle
new file mode 100644
index 000000000..9f778b415
--- /dev/null
+++ b/app-icon-loader/build.gradle
@@ -0,0 +1,60 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-kapt'
+
+android {
+ compileSdkVersion 32
+ buildToolsVersion "32.0.0"
+ ndkVersion "21.4.7075529"
+
+ defaultConfig {
+ applicationId "com.kunzisoft.keepass.icons"
+ minSdkVersion 15
+ targetSdkVersion 32
+ versionCode = 114
+ versionName = "3.4.5"
+
+ kapt {
+ arguments {
+ arg("room.incremental", "true")
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled = false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ testOptions {
+ unitTests.includeAndroidResources = true
+ }
+
+ compileOptions {
+ targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+def room_version = "2.4.2"
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+
+ // Androidx Core Graphics
+ implementation "androidx.core:core-ktx:$android_core_version"
+
+ // Database
+ implementation "androidx.room:room-runtime:$room_version"
+ kapt "androidx.room:room-compiler:$room_version"
+
+ implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
+ implementation("com.squareup.okhttp3:okhttp")
+}
diff --git a/app-icon-loader/lint.xml b/app-icon-loader/lint.xml
new file mode 100644
index 000000000..ee1629d26
--- /dev/null
+++ b/app-icon-loader/lint.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-icon-loader/schemas/com.kunzisoft.keepass.icons.loader.IconDatabase/1.json b/app-icon-loader/schemas/com.kunzisoft.keepass.icons.loader.IconDatabase/1.json
new file mode 100644
index 000000000..1a4149a14
--- /dev/null
+++ b/app-icon-loader/schemas/com.kunzisoft.keepass.icons.loader.IconDatabase/1.json
@@ -0,0 +1,63 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "59d0e9ab5e46349b7903321c59da5e1f",
+ "entities": [
+ {
+ "tableName": "Icon",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` BLOB NOT NULL, `name` TEXT NOT NULL, `sourceKey` TEXT NOT NULL, `source` TEXT NOT NULL, PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sourceKey",
+ "columnName": "sourceKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "source",
+ "columnName": "source",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "uuid"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Icon_sourceKey_source",
+ "unique": true,
+ "columnNames": [
+ "sourceKey",
+ "source"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Icon_sourceKey_source` ON `${TABLE_NAME}` (`sourceKey`, `source`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '59d0e9ab5e46349b7903321c59da5e1f')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app-icon-loader/src/main/AndroidManifest.xml b/app-icon-loader/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..50213f2b1
--- /dev/null
+++ b/app-icon-loader/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDatabase.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDatabase.kt
new file mode 100644
index 000000000..2cff49b24
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDatabase.kt
@@ -0,0 +1,43 @@
+package com.kunzisoft.keepass.icons.loader
+
+import android.database.Cursor
+import androidx.room.*
+import java.util.*
+
+@Database(version = 1, entities = [Icon::class])
+abstract class IconDatabase : RoomDatabase() {
+ abstract fun icons(): IconDao
+}
+
+@Dao
+interface IconDao {
+
+ @Insert
+ fun insert(icons: List)
+
+ @Query("SELECT sourceKey FROM icon WHERE source = :source")
+ fun getSourceKeys(source: IconSource): List
+
+ @Query("SELECT * FROM icon WHERE uuid = :uuid")
+ fun get(uuid: UUID): Icon?
+
+ @Query("SELECT uuid, name, source FROM icon WHERE sourceKey IN (:packageNames) OR sourceKey IN (:hosts)")
+ fun search(packageNames: Set, hosts: Set): Cursor
+}
+
+@Entity(
+ indices = [
+ Index(value = ["sourceKey", "source"], unique = true),
+ ]
+)
+data class Icon(
+ @PrimaryKey
+ val uuid: UUID,
+ val name: String,
+ val sourceKey: String,
+ val source: IconSource,
+)
+
+enum class IconSource {
+ App, DuckDuckGo, Google
+}
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDownloader.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDownloader.kt
new file mode 100644
index 000000000..b9609716e
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDownloader.kt
@@ -0,0 +1,9 @@
+package com.kunzisoft.keepass.icons.loader
+
+interface IconDownloader {
+ fun download(item: T): Icon?
+}
+
+interface IconsDownloader {
+ fun download(items: Set): List
+}
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconWriter.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconWriter.kt
new file mode 100644
index 000000000..b31362c0f
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconWriter.kt
@@ -0,0 +1,26 @@
+package com.kunzisoft.keepass.icons.loader
+
+import android.graphics.Bitmap
+import java.io.File
+import java.io.FileOutputStream
+
+/**
+ * Write downloaded icon to cache directory.
+ */
+class IconWriter(
+ private val iconsDir: File,
+) {
+
+ init {
+ iconsDir.deleteRecursively()
+ iconsDir.mkdirs()
+ }
+
+ fun write(icon: Icon, bitmap: Bitmap) {
+ FileOutputStream(getFile(icon)).use { out ->
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
+ }
+ }
+
+ fun getFile(icon: Icon) = File(iconsDir, "${icon.uuid}.png")
+}
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/KeePassIconsProvider.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/KeePassIconsProvider.kt
new file mode 100644
index 000000000..4dffb1116
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/KeePassIconsProvider.kt
@@ -0,0 +1,101 @@
+package com.kunzisoft.keepass.icons.loader
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import androidx.core.content.ContentProviderCompat.requireContext
+import androidx.room.Room
+import com.kunzisoft.keepass.icons.loader.app.AppIconDownloader
+import com.kunzisoft.keepass.icons.loader.web.DuckDuckGoIconDownloader
+import com.kunzisoft.keepass.icons.loader.web.GoogleWebIconDownloader
+import java.io.File
+import java.util.*
+
+/**
+ * Request icons for KeePassDX via this [ContentProvider].
+ */
+class KeePassIconsProvider : ContentProvider() {
+
+ private val pm by lazy {
+ requireContext(this).packageManager
+ }
+
+ private val db by lazy {
+ Room.inMemoryDatabaseBuilder(requireContext(this), IconDatabase::class.java).build()
+ }
+
+ private val icons by lazy {
+ db.icons()
+ }
+
+ private val appIconDownloader by lazy {
+ AppIconDownloader(pm, icons, writer)
+ }
+
+ private val duckDuckGoIconDownloader by lazy {
+ DuckDuckGoIconDownloader(icons, writer)
+ }
+
+ private val googleWebIconDownloader by lazy {
+ GoogleWebIconDownloader(icons, writer)
+ }
+
+ private val writer by lazy {
+ IconWriter(iconsDir = File(requireContext(this).cacheDir, "/icons/"))
+ }
+
+ override fun onCreate(): Boolean = true
+
+ override fun getType(uri: Uri): String? = null
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?,
+ ): Cursor {
+ val args = selectionArgs?.asSequence().orEmpty()
+ val packageNames = args.filter(prefix = "app:").toSet()
+ val hosts = args.filter(prefix = "host:").toSet()
+
+ // Download all icons
+ val appIcons = appIconDownloader.download(packageNames)
+ val duckDuckGoIcons = duckDuckGoIconDownloader.download(hosts)
+ val googleUrlIcons = googleWebIconDownloader.download(hosts)
+
+ // Update icon database
+ icons.insert(icons = appIcons + duckDuckGoIcons + googleUrlIcons)
+
+ // Query database
+ return icons.search(
+ packageNames = packageNames,
+ hosts = hosts,
+ )
+ }
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?,
+ ): Int = 0
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? =
+ icons.get(
+ uuid = UUID.fromString(uri.pathSegments.last())
+ )?.let { icon ->
+ ParcelFileDescriptor.open(writer.getFile(icon), ParcelFileDescriptor.MODE_READ_ONLY)
+ }
+
+ private fun Sequence.filter(prefix: String) = this
+ .filter { it.startsWith(prefix) }
+ .map { it.substring(prefix.length) }
+ .filter(String::isNotBlank)
+}
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/app/AppIconDownloader.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/app/AppIconDownloader.kt
new file mode 100644
index 000000000..b3a2ddbb5
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/app/AppIconDownloader.kt
@@ -0,0 +1,39 @@
+package com.kunzisoft.keepass.icons.loader.app
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import androidx.core.graphics.drawable.toBitmap
+import com.kunzisoft.keepass.icons.loader.*
+import java.util.*
+
+/**
+ * Download app icon from [PackageManager].
+ */
+class AppIconDownloader(
+ private val pm: PackageManager,
+ private val db: IconDao,
+ private val writer: IconWriter,
+) : IconsDownloader, IconDownloader {
+
+ private val source = IconSource.App
+
+ override fun download(items: Set): List {
+ val existingIcons = db.getSourceKeys(source)
+ return pm.getInstalledPackages(0)
+ .filter { items.contains(it.packageName) }
+ .filterNot { existingIcons.contains(it.packageName) }
+ .map { packageInfo -> download(packageInfo) }
+ }
+
+ override fun download(item: PackageInfo): Icon =
+ Icon(
+ uuid = UUID.randomUUID(),
+ name = item.applicationInfo?.loadLabel(pm)?.toString() ?: item.packageName,
+ sourceKey = item.packageName,
+ source = source,
+ ).also { icon ->
+ val appIcon = pm.getApplicationIcon(item.packageName)
+ writer.write(icon, appIcon.toBitmap(config = Bitmap.Config.ARGB_8888))
+ }
+}
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/DuckDuckGoIconDownloader.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/DuckDuckGoIconDownloader.kt
new file mode 100644
index 000000000..eaec58a0b
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/DuckDuckGoIconDownloader.kt
@@ -0,0 +1,22 @@
+package com.kunzisoft.keepass.icons.loader.web
+
+import com.kunzisoft.keepass.icons.loader.IconDao
+import com.kunzisoft.keepass.icons.loader.IconSource
+import com.kunzisoft.keepass.icons.loader.IconWriter
+import com.kunzisoft.keepass.icons.loader.IconsDownloader
+import okhttp3.OkHttpClient
+
+/**
+ * Download web icon from DuckDuckGo.
+ */
+class DuckDuckGoIconDownloader(
+ private val db: IconDao,
+ private val writer: IconWriter,
+ private val client: OkHttpClient = OkHttpClient(),
+) : IconsDownloader by WebIconDownloader(
+ source = IconSource.DuckDuckGo,
+ serviceUrl = { host -> "https://icons.duckduckgo.com/ip3/$host.ico" },
+ db = db,
+ writer = writer,
+ client = client,
+)
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/GoogleWebIconDownloader.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/GoogleWebIconDownloader.kt
new file mode 100644
index 000000000..bd1451b94
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/GoogleWebIconDownloader.kt
@@ -0,0 +1,22 @@
+package com.kunzisoft.keepass.icons.loader.web
+
+import com.kunzisoft.keepass.icons.loader.IconDao
+import com.kunzisoft.keepass.icons.loader.IconSource
+import com.kunzisoft.keepass.icons.loader.IconWriter
+import com.kunzisoft.keepass.icons.loader.IconsDownloader
+import okhttp3.OkHttpClient
+
+/**
+ * Download web icon from Google.
+ */
+class GoogleWebIconDownloader(
+ private val db: IconDao,
+ private val writer: IconWriter,
+ private val client: OkHttpClient = OkHttpClient(),
+) : IconsDownloader by WebIconDownloader(
+ source = IconSource.Google,
+ serviceUrl = { host -> "https://s2.googleusercontent.com/s2/favicons?domain=$host&sz=64" },
+ db = db,
+ writer = writer,
+ client = client,
+)
diff --git a/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/WebIconDownloader.kt b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/WebIconDownloader.kt
new file mode 100644
index 000000000..f09d6da19
--- /dev/null
+++ b/app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/WebIconDownloader.kt
@@ -0,0 +1,49 @@
+package com.kunzisoft.keepass.icons.loader.web
+
+import android.graphics.BitmapFactory
+import com.kunzisoft.keepass.icons.loader.*
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.net.URLEncoder
+import java.util.*
+
+/**
+ * Download web icon from Google.
+ */
+class WebIconDownloader(
+ private val source: IconSource,
+ private val serviceUrl: (host: String) -> String,
+ private val db: IconDao,
+ private val writer: IconWriter,
+ private val client: OkHttpClient,
+) : IconsDownloader, IconDownloader {
+
+ override fun download(items: Set): List {
+ val existingIcons = db.getSourceKeys(source)
+ return items
+ .filterNot { existingIcons.contains(it) }
+ .mapNotNull { host -> download(host) }
+ }
+
+ override fun download(item: String): Icon? {
+ val host = URLEncoder.encode(item, Charsets.UTF_8.name())
+ val response = client.newCall(
+ request = Request.Builder()
+ .url(serviceUrl(host))
+ .build()
+ ).execute()
+
+ return response.body?.byteStream()?.use { body ->
+ BitmapFactory.decodeStream(body)?.let { bitmap ->
+ Icon(
+ uuid = UUID.randomUUID(),
+ name = item,
+ sourceKey = item,
+ source = source,
+ ).also { icon ->
+ writer.write(icon, bitmap)
+ }
+ }
+ }
+ }
+}
diff --git a/app-icon-loader/src/main/res/drawable-anydpi/ic_launcher_background.png b/app-icon-loader/src/main/res/drawable-anydpi/ic_launcher_background.png
new file mode 100644
index 000000000..4f5f736f1
Binary files /dev/null and b/app-icon-loader/src/main/res/drawable-anydpi/ic_launcher_background.png differ
diff --git a/app-icon-loader/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app-icon-loader/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..46a88724e
--- /dev/null
+++ b/app-icon-loader/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-icon-loader/src/main/res/drawable/ic_launcher_foreground.png b/app-icon-loader/src/main/res/drawable/ic_launcher_foreground.png
new file mode 100644
index 000000000..40a67c4a2
Binary files /dev/null and b/app-icon-loader/src/main/res/drawable/ic_launcher_foreground.png differ
diff --git a/app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app-icon-loader/src/main/res/values/donottranslate.xml b/app-icon-loader/src/main/res/values/donottranslate.xml
new file mode 100644
index 000000000..4eb545cb8
--- /dev/null
+++ b/app-icon-loader/src/main/res/values/donottranslate.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ KeePassDX IconLoader
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index df3349851..4366ea48e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,8 @@
android:name="android.permission.USE_BIOMETRIC" />
+
- IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
+ val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
+ as? EntryEditFragment?
+ val info = entryEditFragment?.retrieveEntryInfo()
+
+ IconPickerActivity.launch(
+ context = this,
+ previousIcon = iconImage,
+ iconProviderData = info?.toIconProviderData(),
+ resultLauncher = mIconSelectionActivityResultLauncher,
+ )
}
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
@@ -564,18 +570,18 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
- override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
- menu?.findItem(R.id.menu_add_field)?.apply {
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ menu.findItem(R.id.menu_add_field)?.apply {
isEnabled = mAllowCustomFields
isVisible = isEnabled
}
- menu?.findItem(R.id.menu_add_attachment)?.apply {
+ menu.findItem(R.id.menu_add_attachment)?.apply {
// Attachment not compatible below KitKat
isEnabled = !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled
}
- menu?.findItem(R.id.menu_add_otp)?.apply {
+ menu.findItem(R.id.menu_add_otp)?.apply {
// OTP not compatible below KitKat
isEnabled = mAllowOTP
&& !mIsTemplate
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
index b658afdcd..10770dd79 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -424,7 +424,12 @@ class GroupActivity : DatabaseLockActivity(),
}
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
- IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
+ IconPickerActivity.launch(
+ context = this,
+ previousIcon = iconImage,
+ iconProviderData = null,
+ resultLauncher = mIconSelectionActivityResultLauncher,
+ )
}
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt
index 93cf4b9a9..6167ee67f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt
@@ -44,6 +44,7 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
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.model.IconProviderData
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.UriUtil
@@ -100,6 +101,10 @@ class IconPickerActivity : DatabaseLockActivity() {
mIconImage = it
}
+ intent?.getParcelableExtra(EXTRA_ICON_PROVIDER_DATA)?.let {
+ iconPickerViewModel.iconProviderData = it
+ }
+
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
@@ -207,18 +212,18 @@ class IconPickerActivity : DatabaseLockActivity() {
toolbar.updateLockPaddingLeft()
}
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.icon, menu)
return true
}
- override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
- menu?.findItem(R.id.menu_edit)?.apply {
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ menu.findItem(R.id.menu_edit)?.apply {
isEnabled = mIconsSelected.size == 1
isVisible = isEnabled
}
- menu?.findItem(R.id.menu_delete)?.apply {
+ menu.findItem(R.id.menu_delete)?.apply {
isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled
}
@@ -262,30 +267,26 @@ class IconPickerActivity : DatabaseLockActivity() {
iconCustomState.errorStringId = R.string.error_file_to_big
} else {
mDatabase?.buildNewCustomIcon { customIcon, binary ->
- if (customIcon != null) {
- iconCustomState.iconCustom = customIcon
- mDatabase?.let { database ->
- BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
- contentResolver,
- database,
- iconToUploadUri,
- binary)
- when {
- binary == null -> {
- }
- binary.getSize() <= 0 -> {
- }
- database.isCustomIconBinaryDuplicate(binary) -> {
- iconCustomState.errorStringId = R.string.error_duplicate_file
- }
- else -> {
- iconCustomState.error = false
- }
+ iconCustomState.iconCustom = customIcon
+ mDatabase?.let { database ->
+ BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
+ contentResolver,
+ database,
+ iconToUploadUri,
+ binary)
+ when {
+ binary.getSize() <= 0 -> {
+ }
+ database.isCustomIconBinaryDuplicate(binary) -> {
+ iconCustomState.errorStringId = R.string.error_duplicate_file
+ }
+ else -> {
+ iconCustomState.error = false
}
}
- if (iconCustomState.error) {
- mDatabase?.removeCustomIcon(customIcon)
- }
+ }
+ if (iconCustomState.error) {
+ mDatabase?.removeCustomIcon(customIcon)
}
}
}
@@ -330,7 +331,8 @@ class IconPickerActivity : DatabaseLockActivity() {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val EXTRA_ICON = "EXTRA_ICON"
- private const val MAX_ICON_SIZE = 5242880
+ private const val EXTRA_ICON_PROVIDER_DATA = "EXTRA_ICON_PROVIDER_DATA"
+ const val MAX_ICON_SIZE = 5_242_880
fun registerIconSelectionForResult(context: FragmentActivity,
listener: (icon: IconImage) -> Unit): ActivityResultLauncher {
@@ -343,13 +345,16 @@ class IconPickerActivity : DatabaseLockActivity() {
fun launch(context: FragmentActivity,
previousIcon: IconImage?,
+ iconProviderData: IconProviderData?,
resultLauncher: ActivityResultLauncher) {
// Create an instance to return the picker icon
resultLauncher.launch(
Intent(context, IconPickerActivity::class.java).apply {
- if (previousIcon != null)
- putExtra(EXTRA_ICON, previousIcon)
+ if (previousIcon != null) {
+ putExtra(EXTRA_ICON, previousIcon)
}
+ putExtra(EXTRA_ICON_PROVIDER_DATA, iconProviderData)
+ }
)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt
index 08fb5e20d..ae23d2b21 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt
@@ -309,7 +309,7 @@ class EntryEditFragment: DatabaseFragment() {
setAttachments(entryInfo?.attachments ?: listOf())
}
- private fun retrieveEntryInfo(): EntryInfo {
+ fun retrieveEntryInfo(): EntryInfo {
val entryInfo = templateView.getEntryInfo()
entryInfo.tags = tagsCompletionView.getTags()
entryInfo.attachments = getAttachments().toMutableList()
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt
index e764e429a..6b8e69203 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt
@@ -32,7 +32,7 @@ class IconCustomFragment : IconFragment() {
return R.layout.fragment_icon_grid
}
- override fun defineIconList(database: Database?) {
+ override suspend fun defineIconList(database: Database?) {
database?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt
index f63a7645c..2a7ee069d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt
@@ -47,7 +47,7 @@ abstract class IconFragment : DatabaseFragment(),
abstract fun retrieveMainLayoutId(): Int
- abstract fun defineIconList(database: Database?)
+ abstract suspend fun defineIconList(database: Database?)
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
@@ -75,12 +75,9 @@ abstract class IconFragment : DatabaseFragment(),
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch {
- val populateList = launch {
- iconPickerAdapter.clear()
- defineIconList(database)
- }
+ iconPickerAdapter.clear()
+ defineIconList(database)
withContext(Dispatchers.Main) {
- populateList.join()
iconPickerAdapter.notifyDataSetChanged()
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconLoaderFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconLoaderFragment.kt
new file mode 100644
index 000000000..be841d8fd
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconLoaderFragment.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2021 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.activities.fragments
+
+import android.widget.ProgressBar
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.IconPickerActivity
+import com.kunzisoft.keepass.database.element.Database
+import com.kunzisoft.keepass.database.element.binary.BinaryData
+import com.kunzisoft.keepass.database.element.icon.IconImageCustom
+import com.kunzisoft.keepass.icons.IconDrawableFactory
+import com.kunzisoft.keepass.icons.KeePassIconsProviderClient
+import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
+import com.kunzisoft.keepass.view.hideByFading
+import com.kunzisoft.keepass.view.showByFading
+import com.kunzisoft.keepass.viewmodels.IconPickerViewModel.IconCustomState
+import kotlinx.coroutines.*
+import kotlin.coroutines.resume
+
+class IconLoaderFragment : IconFragment() {
+
+ private val mainScope = CoroutineScope(Dispatchers.Main)
+
+ private val client by lazy {
+ KeePassIconsProviderClient(requireContext().contentResolver)
+ }
+
+ override fun retrieveMainLayoutId(): Int {
+ return R.layout.fragment_icon_grid
+ }
+
+ override fun onDatabaseRetrieved(database: Database?) {
+ super.onDatabaseRetrieved(database)
+
+ // Change IconDrawableFactory for IconLoader
+ iconPickerAdapter.iconDrawableFactory = IconDrawableFactory(
+ retrieveBinaryCache = { client.cache },
+ retrieveCustomIconBinary = { iconId -> client.loadIcon(iconId) },
+ )
+ }
+
+ override suspend fun defineIconList(database: Database?) {
+ val iconProviderData = iconPickerViewModel.iconProviderData
+ if (iconProviderData != null) {
+ showLoading(true)
+ client.queryIcons(iconProviderData).forEach {
+ iconPickerAdapter.addIcon(it, false)
+ }
+ showLoading(false)
+ }
+ }
+
+ override fun onIconClickListener(icon: IconImageCustom) {
+ mDatabase?.let { database ->
+ mainScope.launch {
+ val iconCustomState = addCustomIcon(database, icon)
+ val iconCustom = iconCustomState.iconCustom
+ if (iconCustom != null) {
+ iconPickerViewModel.pickCustomIcon(iconCustom)
+ } else {
+ iconPickerViewModel.addCustomIcon(iconCustomState)
+ }
+ }
+ }
+ }
+
+ override fun onIconLongClickListener(icon: IconImageCustom) {}
+
+ private suspend fun showLoading(show: Boolean) = Dispatchers.Main {
+ val loading = requireView().findViewById(R.id.loading)
+ if (show) loading.showByFading() else loading.hideByFading()
+ }
+
+ private suspend fun addCustomIcon(database: Database, icon: IconImageCustom) = Dispatchers.IO {
+ val binaryData = client.loadIcon(icon.uuid)
+
+ val iconCustomState = when {
+ binaryData == null ->
+ IconCustomState(errorStringId = R.string.error_upload_file)
+
+ binaryData.getSize() > IconPickerActivity.MAX_ICON_SIZE ->
+ IconCustomState(errorStringId = R.string.error_file_to_big)
+
+ else ->
+ addCustomIcon(database, icon, binaryData)
+ }
+ iconCustomState
+ }
+
+ private suspend fun addCustomIcon(
+ database: Database,
+ icon: IconImageCustom,
+ binaryData: BinaryData,
+ ): IconCustomState = suspendCancellableCoroutine { continuation ->
+ database.buildNewCustomIcon { customIcon, binary ->
+ BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
+ database = database,
+ inputStream = binaryData.getInputDataStream(client.cache),
+ binaryData = binary,
+ )
+
+ val iconCustomState = when {
+ binary.getSize() <= 0 ->
+ IconCustomState(errorStringId = R.string.error_upload_file)
+
+ database.isCustomIconBinaryDuplicate(binary) ->
+ IconCustomState(errorStringId = R.string.error_duplicate_file)
+
+ else ->
+ IconCustomState(
+ iconCustom = customIcon.apply {
+ name = icon.name
+ },
+ error = false,
+ )
+ }
+
+ if (iconCustomState.error) {
+ database.removeCustomIcon(customIcon)
+ }
+
+ continuation.resume(iconCustomState)
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt
index fbc84de85..47f4368d7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt
@@ -1,5 +1,6 @@
package com.kunzisoft.keepass.activities.fragments
+import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -11,16 +12,23 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database
+import com.kunzisoft.keepass.icons.KeePassIconsProviderClient
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : DatabaseFragment() {
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
+ private var isIconsProviderInstalled: Boolean = false
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ isIconsProviderInstalled = KeePassIconsProviderClient(context.contentResolver).exists()
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -36,28 +44,34 @@ class IconPickerFragment : DatabaseFragment() {
tabLayout = view.findViewById(R.id.tabs_layout)
resetAppTimeoutWhenViewFocusedOrChanged(view)
- arguments?.apply {
- if (containsKey(ICON_TAB_ARG)) {
- viewPager.currentItem = getInt(ICON_TAB_ARG)
- }
- remove(ICON_TAB_ARG)
- }
-
- iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
+ iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) {
viewPager.currentItem = 1
}
}
override fun onDatabaseRetrieved(database: Database?) {
- iconPickerPagerAdapter = IconPickerPagerAdapter(this,
- if (database?.allowCustomIcons == true) 2 else 1)
+ val size = when (database?.allowCustomIcons) {
+ null, false -> 1
+ !isIconsProviderInstalled -> 2
+ else -> 3
+ }
+ iconPickerPagerAdapter = IconPickerPagerAdapter(this, size)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
+ 0 -> getString(R.string.icon_section_standard)
1 -> getString(R.string.icon_section_custom)
- else -> getString(R.string.icon_section_standard)
+ 2 -> getString(R.string.icon_section_loader)
+ else -> error("Invalid position '$position'.")
}
}.attach()
+
+ arguments?.apply {
+ if (containsKey(ICON_TAB_ARG)) {
+ viewPager.currentItem = getInt(ICON_TAB_ARG)
+ }
+ remove(ICON_TAB_ARG)
+ }
}
enum class IconTab {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt
index dd16578c4..5cce7cd80 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt
@@ -30,7 +30,7 @@ class IconStandardFragment : IconFragment() {
return R.layout.fragment_icon_grid
}
- override fun defineIconList(database: Database?) {
+ override suspend fun defineIconList(database: Database?) {
database?.doForEachStandardIcons { standardIcon ->
iconPickerAdapter.addIcon(standardIcon, false)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt
index e06ab0c86..34186e903 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerPagerAdapter.kt
@@ -3,6 +3,7 @@ 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.IconLoaderFragment
import com.kunzisoft.keepass.activities.fragments.IconStandardFragment
class IconPickerPagerAdapter(fragment: Fragment, val size: Int)
@@ -10,15 +11,14 @@ class IconPickerPagerAdapter(fragment: Fragment, val size: Int)
private val iconStandardFragment = IconStandardFragment()
private val iconCustomFragment = IconCustomFragment()
+ private val iconLoaderFragment = IconLoaderFragment()
- override fun getItemCount(): Int {
- return size
- }
+ override fun getItemCount(): Int = size
- override fun createFragment(position: Int): Fragment {
- return when (position) {
- 1 -> iconCustomFragment
- else -> iconStandardFragment
- }
+ override fun createFragment(position: Int): Fragment = when (position) {
+ 0 -> iconStandardFragment
+ 1 -> iconCustomFragment
+ 2 -> iconLoaderFragment
+ else -> error("Invalid position '$position'.")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
index f38f7df92..87e89ccfe 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
@@ -133,7 +133,7 @@ class Database {
return iconsManager.getIcon(iconId)
}
- fun buildNewCustomIcon(result: (IconImageCustom?, BinaryData?) -> Unit) {
+ fun buildNewCustomIcon(result: (IconImageCustom, BinaryData) -> Unit) {
mDatabaseKDBX?.buildNewCustomIcon(null, result)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt
index 7b5ae7892..6aaabafdb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt
@@ -12,7 +12,7 @@ class CustomIconPool : BinaryPool() {
name: String,
lastModificationTime: DateInstant?,
builder: (uniqueBinaryId: String) -> BinaryData,
- result: (IconImageCustom, BinaryData?) -> Unit) {
+ result: (IconImageCustom, BinaryData) -> Unit) {
val keyBinary = super.put(key, builder)
val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt
index 5440faf64..87fff87a1 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt
@@ -456,7 +456,7 @@ class DatabaseKDBX : DatabaseVersioned {
}
fun buildNewCustomIcon(customIconId: UUID? = null,
- result: (IconImageCustom, BinaryData?) -> Unit) {
+ result: (IconImageCustom, BinaryData) -> Unit) {
// Create a binary file for a brand new custom icon
addCustomIcon(customIconId, "", null, false, result)
}
@@ -465,7 +465,7 @@ class DatabaseKDBX : DatabaseVersioned {
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
- result: (IconImageCustom, BinaryData?) -> Unit) {
+ result: (IconImageCustom, BinaryData) -> Unit) {
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, { uniqueBinaryId ->
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt
index 6f6b88d4f..f41cc7566 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt
@@ -54,7 +54,7 @@ class IconsManager {
name: String,
lastModificationTime: DateInstant?,
builder: (uniqueBinaryId: String) -> BinaryData,
- result: (IconImageCustom, BinaryData?) -> Unit) {
+ result: (IconImageCustom, BinaryData) -> Unit) {
customCache.put(key, name, lastModificationTime, builder, result)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt
index ec0ca0e50..1490d6430 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt
@@ -701,7 +701,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
customIconName,
customIconLastModificationTime,
isRAMSufficient.invoke(iconData.size.toLong())) { _, binary ->
- binary?.getOutputDataStream(mDatabase.binaryCache)?.use { outputStream ->
+ binary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
outputStream.write(iconData)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt b/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt
index 25c3847ce..bac5a2d8d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt
@@ -281,9 +281,9 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
false
) { _, newBinaryData ->
binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
- newBinaryData?.getOutputDataStream(database.binaryCache).use { outputStream ->
+ newBinaryData.getOutputDataStream(database.binaryCache).use { outputStream ->
inputStream.readAllBytes { buffer ->
- outputStream?.write(buffer)
+ outputStream.write(buffer)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt
index 48b7dde1b..358374b9f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt
@@ -33,18 +33,13 @@ 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.Database
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
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 kotlinx.coroutines.*
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
@@ -99,25 +94,23 @@ class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
/**
* Build a custom [Drawable] from custom [icon]
*/
- private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? {
+ private fun getIconDrawable(resources: Resources, icon: IconImageCustom, binaryFile: BinaryData): Drawable? {
val patternIcon = PatternIcon(resources)
val binaryManager = retrieveBinaryCache()
if (binaryManager != null) {
val draw: Drawable? = customIconMap[icon.uuid]?.get()
if (draw == null) {
- iconCustomBinary?.let { binaryFile ->
- try {
- var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(binaryManager))
- 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)
+ try {
+ var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(binaryManager))
+ bitmap?.let { bitmapIcon ->
+ bitmap = resize(bitmapIcon, patternIcon)
+ val createdDraw = BitmapDrawable(resources, bitmap)
+ customIconMap[icon.uuid] = WeakReference(createdDraw)
+ return createdDraw
}
+ } catch (e: Exception) {
+ customIconMap.remove(icon.uuid)
+ Log.e(TAG, "Unable to create the bitmap icon", e)
}
} else {
return draw
@@ -176,15 +169,22 @@ class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
fun assignDatabaseIcon(imageView: ImageView,
icon: IconImageDraw,
tintColor: Int = Color.WHITE) {
+ val context = imageView.context
+
+ // Cancel ongoing download and reset icon
+ (imageView.tag as? Job)?.cancel()
+ imageView.setImageResource(R.drawable.ic_downloading_white_24dp)
+ ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
+
try {
- val context = imageView.context
- CoroutineScope(Dispatchers.IO).launch {
+ // Start download of new icon in background
+ imageView.tag = CoroutineScope(Dispatchers.IO).launch {
addToCustomCache(context.resources, icon)
+ val superDrawable = getIconSuperDrawable(context,
+ icon,
+ imageView.width,
+ tintColor)
withContext(Dispatchers.Main) {
- val superDrawable = getIconSuperDrawable(context,
- icon,
- imageView.width,
- tintColor)
imageView.setImageDrawable(superDrawable.drawable)
if (superDrawable.tintable) {
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tintColor))
diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/KeePassIconsProviderClient.kt b/app/src/main/java/com/kunzisoft/keepass/icons/KeePassIconsProviderClient.kt
new file mode 100644
index 000000000..782606d4e
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/icons/KeePassIconsProviderClient.kt
@@ -0,0 +1,76 @@
+package com.kunzisoft.keepass.icons
+
+import android.content.ContentResolver
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import com.kunzisoft.keepass.database.element.binary.BinaryByte
+import com.kunzisoft.keepass.database.element.binary.BinaryCache
+import com.kunzisoft.keepass.database.element.binary.BinaryData
+import com.kunzisoft.keepass.database.element.icon.IconImageCustom
+import com.kunzisoft.keepass.model.IconProviderData
+import java.io.FileInputStream
+import java.nio.ByteBuffer
+import java.util.*
+
+private const val AUTHORITY = "com.kunzisoft.keepass.icons.loader"
+
+class KeePassIconsProviderClient(
+ private val contentResolver: ContentResolver,
+ val cache: BinaryCache = BinaryCache(),
+) {
+
+ fun exists() =
+ contentResolver.acquireContentProviderClient(AUTHORITY).let {
+ @Suppress("DEPRECATION")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) it?.close() else it?.release()
+ it != null
+ }
+
+ fun queryIcons(iconProviderData: IconProviderData): List {
+ val packageNames = iconProviderData.packageNames
+ val hosts = iconProviderData.hosts
+ val selectionArgs = packageNames.map { "app:$it" } + hosts.map { "host:$it" }
+
+ return contentResolver.query(
+ Uri.parse("content://$AUTHORITY"),
+ null,
+ null,
+ selectionArgs.toSet().toTypedArray(),
+ null
+ )?.use { cursor ->
+ cursor.asSequence().map {
+ val uuid = it.getBlob(0).toUUID()
+ val name = it.getString(1)
+ IconImageCustom(
+ uuid = uuid,
+ name = name,
+ )
+ }.toList()
+ } ?: emptyList()
+ }
+
+ fun loadIcon(iconId: UUID): BinaryData? =
+ contentResolver.openFileDescriptor(
+ Uri.parse("content://$AUTHORITY/$iconId"),
+ "r",
+ ).use { file ->
+ file?.fileDescriptor?.let { fileDescriptor ->
+ BinaryByte(iconId.toString()).apply {
+ getOutputDataStream(cache).use { out ->
+ FileInputStream(fileDescriptor).copyTo(out)
+ }
+ }
+ }
+ }
+
+ private fun ByteArray.toUUID(): UUID {
+ val buffer = ByteBuffer.wrap(this)
+ val firstLong = buffer.long
+ val secondLong = buffer.long
+ return UUID(firstLong, secondLong)
+ }
+
+ private fun Cursor.asSequence(): Sequence =
+ generateSequence { takeIf { it.moveToNext() } }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/IconProviderData.kt b/app/src/main/java/com/kunzisoft/keepass/model/IconProviderData.kt
new file mode 100644
index 000000000..cea682c13
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/model/IconProviderData.kt
@@ -0,0 +1,35 @@
+package com.kunzisoft.keepass.model
+
+import android.net.Uri
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class IconProviderData(
+ val packageNames: List,
+ val hosts: List,
+) : Parcelable
+
+fun EntryInfo.toIconProviderData(): IconProviderData {
+ val packageNames = customFields
+ .asSequence()
+ .filter { it.name == "AndroidApp" || it.name.matches("AndroidApp_\\d+".toRegex()) }
+ .map { it.protectedValue.stringValue }
+ .filter(String::isNotBlank)
+
+ val urls = sequenceOf(url) + customFields
+ .asSequence()
+ .filter { it.name.matches("URL_\\d+".toRegex()) }
+ .map { it.protectedValue.stringValue }
+
+ val hosts = urls
+ .mapNotNull { url ->
+ Uri.parse(url).host ?: Uri.parse("//$url").host
+ }
+ .filter(String::isNotBlank)
+
+ return IconProviderData(
+ packageNames = packageNames.toList(),
+ hosts = hosts.toList(),
+ )
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt
index b561ba232..7b3143f36 100644
--- a/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt
@@ -5,11 +5,11 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
-import com.kunzisoft.keepass.utils.readAllBytes
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.utils.UriUtil
+import com.kunzisoft.keepass.utils.readAllBytes
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -94,30 +94,35 @@ object BinaryDatabaseManager {
fun resizeBitmapAndStoreDataInBinaryFile(contentResolver: ContentResolver,
database: Database,
bitmapUri: Uri?,
- binaryData: BinaryData?) {
+ 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(
- database.binaryCache,
- byteArrayInputStream,
- bitmapData.size.toLong(),
- binaryData
- )
- }
- }
+ UriUtil.getUriInputStream(contentResolver, bitmapUri)?.use { inputStream ->
+ resizeBitmapAndStoreDataInBinaryFile(database, inputStream, binaryData)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to resize bitmap to store it in binary", e)
}
}
+ fun resizeBitmapAndStoreDataInBinaryFile(
+ database: Database,
+ inputStream: InputStream,
+ binaryData: BinaryData,
+ ) {
+ 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(
+ database.binaryCache,
+ byteArrayInputStream,
+ bitmapData.size.toLong(),
+ binaryData
+ )
+ }
+ }
/**
* reduces the size of the image
diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt
index 38f3850d8..b541d7807 100644
--- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt
@@ -2,15 +2,16 @@ package com.kunzisoft.keepass.viewmodels
import android.os.Parcel
import android.os.Parcelable
-import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
-
+import com.kunzisoft.keepass.model.IconProviderData
class IconPickerViewModel: ViewModel() {
+ var iconProviderData: IconProviderData? = null
+
val standardIconPicked: MutableLiveData by lazy {
MutableLiveData()
}
diff --git a/app/src/main/res/layout/fragment_icon_grid.xml b/app/src/main/res/layout/fragment_icon_grid.xml
index 6f3775a40..50f50eafa 100644
--- a/app/src/main/res/layout/fragment_icon_grid.xml
+++ b/app/src/main/res/layout/fragment_icon_grid.xml
@@ -1,5 +1,4 @@
-
-
-
+ android:layout_height="match_parent">
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_icon.xml b/app/src/main/res/layout/item_icon.xml
index 9e6469044..b7b137433 100644
--- a/app/src/main/res/layout/item_icon.xml
+++ b/app/src/main/res/layout/item_icon.xml
@@ -19,14 +19,16 @@
-->
+ tools:layout_width="80dp">
+ android:layout_marginRight="16dp"
+ tools:src="@android:drawable/sym_def_app_icon" />
+ android:paddingRight="4dp"
+ tools:text="Long Icon Name that requires multiple lines" />
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 5f2128c1d..c6f39fa70 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -560,8 +560,9 @@
Hierher kann keine Gruppe verschoben werden.
Ausfüllvorschläge
Dieses Wort ist reserviert und kann nicht verwendet werden.
- Benutzerdefiniert
+ Benutzer
Standard
+ Extern
Helles oder dunkles Design auswählen
Designhelligkeit
GiB
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index aab95a64c..e5458a90e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -696,6 +696,7 @@
Standard
Custom
+ External
Icon pack
Icon pack used in the app
Entry colors
diff --git a/settings.gradle b/settings.gradle
index a8916a099..00cfbd41b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':icon-pack-classic', ':icon-pack-material', ':crypto'
+include ':app', ':app-icon-loader', ':icon-pack-classic', ':icon-pack-material', ':crypto'