From 3cee51948946672853abf7884fc2432d003719f5 Mon Sep 17 00:00:00 2001 From: Sven Obser Date: Sat, 26 Nov 2022 16:41:01 +0100 Subject: [PATCH 1/3] Simplify nullability --- .../keepass/activities/IconPickerActivity.kt | 40 +++++++++---------- .../keepass/database/element/Database.kt | 2 +- .../database/element/binary/CustomIconPool.kt | 2 +- .../database/element/database/DatabaseKDBX.kt | 4 +- .../database/element/icon/IconsManager.kt | 2 +- .../database/file/input/DatabaseInputKDBX.kt | 2 +- .../database/merge/DatabaseKDBXMerger.kt | 4 +- .../keepass/icons/IconDrawableFactory.kt | 24 +++++------ .../keepass/tasks/BinaryDatabaseManager.kt | 30 +++++++------- 9 files changed, 51 insertions(+), 59 deletions(-) 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..7a6a87605 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt @@ -262,30 +262,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) } } } 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..d40462752 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt @@ -99,25 +99,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 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..3addf0c08 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/BinaryDatabaseManager.kt @@ -94,23 +94,21 @@ 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 -> + 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 + ) } } } catch (e: Exception) { From 828ab7f76a229fc0dea02b01f1b4bffb9ea46e71 Mon Sep 17 00:00:00 2001 From: Sven Obser Date: Sun, 27 Nov 2022 21:08:31 +0100 Subject: [PATCH 2/3] Fix some errors and warnings --- .../com/kunzisoft/keepass/activities/EntryEditActivity.kt | 8 ++++---- .../kunzisoft/keepass/activities/IconPickerActivity.kt | 8 ++++---- .../keepass/activities/fragments/IconPickerFragment.kt | 2 +- .../kunzisoft/keepass/viewmodels/IconPickerViewModel.kt | 2 -- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 89e0252f5..30a5e2885 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -564,18 +564,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/IconPickerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt index 7a6a87605..cb8d564b4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt @@ -207,18 +207,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 } 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..edb28976b 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 @@ -43,7 +43,7 @@ class IconPickerFragment : DatabaseFragment() { remove(ICON_TAB_ARG) } - iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ -> + iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { viewPager.currentItem = 1 } } 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..f1ae8f0c9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt @@ -2,13 +2,11 @@ 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 - class IconPickerViewModel: ViewModel() { val standardIconPicked: MutableLiveData by lazy { From 27edacdbf7563458bdc4c60db9002b1d3f684100 Mon Sep 17 00:00:00 2001 From: Sven Obser Date: Sun, 27 Nov 2022 21:08:05 +0100 Subject: [PATCH 3/3] Add a custom content provider that can download icons Closes #596 --- app-icon-loader/.gitignore | 2 + app-icon-loader/build.gradle | 60 ++++++++ app-icon-loader/lint.xml | 6 + .../1.json | 63 ++++++++ app-icon-loader/src/main/AndroidManifest.xml | 29 ++++ .../keepass/icons/loader/IconDatabase.kt | 43 ++++++ .../keepass/icons/loader/IconDownloader.kt | 9 ++ .../keepass/icons/loader/IconWriter.kt | 26 ++++ .../icons/loader/KeePassIconsProvider.kt | 101 +++++++++++++ .../icons/loader/app/AppIconDownloader.kt | 39 +++++ .../loader/web/DuckDuckGoIconDownloader.kt | 22 +++ .../loader/web/GoogleWebIconDownloader.kt | 22 +++ .../icons/loader/web/WebIconDownloader.kt | 49 ++++++ .../ic_launcher_background.png | Bin 0 -> 953 bytes .../drawable-v24/ic_launcher_foreground.xml | 31 ++++ .../res/drawable/ic_launcher_foreground.png | Bin 0 -> 1821 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/values/donottranslate.xml | 24 +++ app/src/main/AndroidManifest.xml | 2 + .../keepass/activities/EntryEditActivity.kt | 16 +- .../keepass/activities/GroupActivity.kt | 7 +- .../keepass/activities/IconPickerActivity.kt | 15 +- .../activities/fragments/EntryEditFragment.kt | 2 +- .../fragments/IconCustomFragment.kt | 2 +- .../activities/fragments/IconFragment.kt | 9 +- .../fragments/IconLoaderFragment.kt | 142 ++++++++++++++++++ .../fragments/IconPickerFragment.kt | 34 +++-- .../fragments/IconStandardFragment.kt | 2 +- .../adapters/IconPickerPagerAdapter.kt | 16 +- .../keepass/icons/IconDrawableFactory.kt | 26 ++-- .../icons/KeePassIconsProviderClient.kt | 76 ++++++++++ .../keepass/model/IconProviderData.kt | 35 +++++ .../keepass/tasks/BinaryDatabaseManager.kt | 35 +++-- .../keepass/viewmodels/IconPickerViewModel.kt | 3 + .../main/res/layout/fragment_icon_grid.xml | 30 +++- app/src/main/res/layout/item_icon.xml | 15 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values/strings.xml | 1 + settings.gradle | 2 +- 40 files changed, 932 insertions(+), 77 deletions(-) create mode 100644 app-icon-loader/.gitignore create mode 100644 app-icon-loader/build.gradle create mode 100644 app-icon-loader/lint.xml create mode 100644 app-icon-loader/schemas/com.kunzisoft.keepass.icons.loader.IconDatabase/1.json create mode 100644 app-icon-loader/src/main/AndroidManifest.xml create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDatabase.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconDownloader.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/IconWriter.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/KeePassIconsProvider.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/app/AppIconDownloader.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/DuckDuckGoIconDownloader.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/GoogleWebIconDownloader.kt create mode 100644 app-icon-loader/src/main/java/com/kunzisoft/keepass/icons/loader/web/WebIconDownloader.kt create mode 100644 app-icon-loader/src/main/res/drawable-anydpi/ic_launcher_background.png create mode 100644 app-icon-loader/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app-icon-loader/src/main/res/drawable/ic_launcher_foreground.png create mode 100644 app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app-icon-loader/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app-icon-loader/src/main/res/values/donottranslate.xml create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconLoaderFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/icons/KeePassIconsProviderClient.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/model/IconProviderData.kt 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 0000000000000000000000000000000000000000..4f5f736f1018d8b4f58f13b7df18fef31eb1fc5c GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf2?p zD}XSgpwrRCKtah8*NBqf{Irtt#G+J&^73-M%)IR47aCH34zO4305RS?JpBGYBQr~E z!%j}SJ+~RS`DI<3lYbO!V-%CK>SBBTfpMNPP;%yijT>XvU3|-IlI3zyW6i?r>={=& z79FgwfHB)1n5$JZq!)d+Z`&Z%$QTys#qhD!T^PuImf#m%aMc zaH`3-QeW!Dq4w2w3l=7n-s@)j^&#;~qtBuJXZo!tc>ZwxX~=HGx!)rGen%vOFpJ$4 zvm~(t7FnD07OxUCW;hl-Idqd6`{uf%Jhvxi&(k}=vRPKra>mlo#evG~n`h)(8}<1g z(P`k!u{Qeh8|aUNKo`wwj=Ym?ZS;SNej3NUwTTDIQtH+o)ajKvCH0yoE%A6+${NEN zt-e1lTxK`n^1FUOucpIpMr-a5fl34J-yark;|~z<`SH7E&q19(2ih~5BY!mUPh{Kt zfn9zPPufFaI~R#FhxBVc0JY@*cmUM$|3l&t-=CcMr*4IxvX%N*XXw4SO_4XgIr7K` z*`3cUqc@n!pDcSNWyqcHwYV*i_cu@i=!LiDPcN5+n7sLyG|i2dUCNAob4RYVQHg$r z#+3-5gYR9_Ps_Yguq-j%pu053{n>B*j1VrlqgmUOAO8b+rO))8?3pCr+Y>LAIWBho zpdGeNR`NR!Pz%Q*!+L}6EwVH9_b*teFk{>2S@X?jEDcm%aBR*&ol9lsK0S|j*XCUI zcF+0S_7gbQ8Gcz-bL(7b-3?%T=%41d1jffxsaNJth3n?Fy#k7@za9XJqg*L1kl1dw z??AElmtR-`#r{g21&ZzKbGr-_yMB3v9Z+nSR47PnpW8>EnEc|v5{1bhj%UW)o8tc? z+4NqX!y)-&y9WK;Q5DWxwx;luN+H)aYC&tLy>uc4*B{}{8-zqwNLHyZfQ UiM_0<1I%U&p00i_>zopr0EWM*r2qf` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..40a67c4a2e6f865f85f7fe66faaf31aae76e9b00 GIT binary patch literal 1821 zcmah~`8ON*7XK2`SW-!=gc@Qgq4uq%N@xh$DjqFbOKRz;r8GgML5i`fGSt3Q?L=t} zL!!JE(=i&=MG&=AEfd>`)<`O9^dETdp8L6Q zv$3v4Xz9R?@IT2u_JeoeFXUsajZ<)0c@aZW*Qp}sN~F0u@{;LC-Uw$OLm+G<5s7|| z_9tE6Ua#qu(YE(}(mS*Maf3$)pP%yC%ue#2v;W@44HBBFl8^-F}+(^ibT~F)=Y&2=I)zbUB+s5Yh8t@IL&r&c0G+ z9(6MgBi)1K7y1i$-1A(jT{71X*a;Pp`i&wBz8^UHDv=UGxW90}h_&inG*W%{pS8(s z5Cn>#e&oQwz-|JHz%<+0-McG<*=d+fYP$`M00gjiUh9eCS<>_UUSv?Bx#Qpl68GR> zyK(VwXXxZ@fIUTZ0^W)_^_D2f?+}pHP63A6MWUDKRr-50QJ=YHTb~Arcov;?x!f#Xz`G|ZA(Kob5;gvM zYC(3sm+~lzL@Jx;3}YWUu)1%hXGUUvD^EOd6B?bKXs7b`3Z{lgYdygm7vwNB(9si| zrPrkrnHd@CjZIBHeerusCIUEiJG3KN+@Jgz`S`^jR2e5f65r9% zc!6`M9yXu*Oy!B-&kV$G{+&9VGUycUMMV^Ht>dU74vp|wBbkTjPxHW>mZs)p?Ak=T zX3T@(PSH+jwBgo{aKsRQ#Duss(g?n7+(3qEU=_NIvYat$*2($7wNKZnv~g-0yKxAn z+Sl8=7ZDY;t?Q7-n`rlkWM^l~nubli7dLot&0ePl2Httd~;BeVFz$k|H*po0#fGC2UI>#On>LoZ9OxwsbO03eI?w z%2V;+q|lyMRKWd&agw)YCgu9W3V%(f>{XcveQFy*YOtltqqe6Muzgo{vJ1>sml-~ZI + + + + 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 -> 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 cb8d564b4..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) @@ -326,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 { @@ -339,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 edb28976b..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) { 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/icons/IconDrawableFactory.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt index d40462752..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 @@ -174,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 3addf0c08..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 @@ -97,25 +97,32 @@ object BinaryDatabaseManager { binaryData: BinaryData) { try { 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 - ) - } + 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 f1ae8f0c9..b541d7807 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/IconPickerViewModel.kt @@ -6,9 +6,12 @@ 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'