diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt index 648a2413..ae02e393 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt @@ -237,13 +237,23 @@ private fun IconLayer( ) { when (layer) { is ClockLayer -> { - ClockLayer(layer.sublayers, scale = layer.scale, tintColor = null) + ClockLayer( + layer.sublayers, + scale = layer.scale, + defaultSecond = layer.defaultSecond, + defaultMinute = layer.defaultMinute, + defaultHour = layer.defaultHour, + tintColor = null + ) } is TintedClockLayer -> { ClockLayer( layer.sublayers, scale = layer.scale, + defaultSecond = layer.defaultSecond, + defaultMinute = layer.defaultMinute, + defaultHour = layer.defaultHour, tintColor = if (layer.color == 0) defaultTintColor else Color(getTone(layer.color, colorTone)) ) @@ -323,6 +333,9 @@ private fun getTone(argb: Int, tone: Int): Int { @Composable private fun ClockLayer( sublayers: List, + defaultMinute: Int, + defaultHour: Int, + defaultSecond: Int, scale: Float, tintColor: Color?, ) { @@ -331,21 +344,22 @@ private fun ClockLayer( } val second = remember { - Animatable(time.second.toFloat()) + Animatable((time.second - defaultSecond).toFloat()) } val minute = remember { - Animatable(time.minute.toFloat() + time.second.toFloat() / 60f) + Animatable((time.minute - defaultMinute).toFloat() + (time.second - defaultSecond).toFloat() / 60f) } val hour = remember { - Animatable(time.hour.toFloat() + time.minute.toFloat() / 60f) + Animatable((time.hour - defaultHour).toFloat() + (time.minute + defaultMinute).toFloat() / 60f) } LaunchedEffect(time) { - val h = time.hour.toFloat() + time.minute.toFloat() / 60f - val m = time.minute.toFloat() + time.second.toFloat() / 60f - val s = time.second.toFloat() + (time.nano / 1000000f) / 1000f + val h = (time.hour - defaultHour).toFloat() + (time.minute - defaultSecond).toFloat() / 60f + val m = + (time.minute - defaultMinute).toFloat() + (time.second - defaultSecond).toFloat() / 60f + val s = (time.second - defaultSecond).toFloat() + (time.nano / 1000000f) / 1000f second.snapTo(s) hour.snapTo(h) minute.snapTo(m) diff --git a/core/base/src/main/java/de/mm20/launcher2/icons/LauncherIconLayer.kt b/core/base/src/main/java/de/mm20/launcher2/icons/LauncherIconLayer.kt index 5ba76449..47a04465 100644 --- a/core/base/src/main/java/de/mm20/launcher2/icons/LauncherIconLayer.kt +++ b/core/base/src/main/java/de/mm20/launcher2/icons/LauncherIconLayer.kt @@ -15,6 +15,9 @@ data class ColorLayer( data class ClockLayer( val sublayers: List, + val defaultHour: Int = 0, + val defaultMinute: Int = 0, + val defaultSecond: Int = 0, val scale: Float, ) : LauncherIconLayer @@ -38,6 +41,9 @@ data class TintedIconLayer( data class TintedClockLayer( val sublayers: List, + val defaultHour: Int = 0, + val defaultMinute: Int = 0, + val defaultSecond: Int = 0, val scale: Float, val color: Int = 0, ) : LauncherIconLayer diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 37591fb7..d8eb8e34 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -48,7 +48,7 @@ dependencies { implementation(libs.androidx.appcompat) api(libs.androidx.roomruntime) kapt(libs.androidx.roomcompiler) - implementation(libs.androidx.room) + api(libs.androidx.room) implementation(libs.koin.android) implementation(project(":core:i18n")) diff --git a/core/database/schemas/de.mm20.launcher2.database.AppDatabase/21.json b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/21.json new file mode 100644 index 00000000..c11e0c0b --- /dev/null +++ b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/21.json @@ -0,0 +1,492 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "5fa8da4798ebb9b966ac8d260c5bbee3", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "precipitation", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))", + "fields": [ + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "lastUpdate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "symbol" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `packageName` TEXT, `activityName` TEXT, `drawable` TEXT, `extras` TEXT, `iconPack` TEXT NOT NULL, `name` TEXT, `themed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activityName", + "columnName": "activityName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themed", + "columnName": "themed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, `themed` INTEGER NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "themed", + "columnName": "themed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomAttributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SearchAction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))", + "fields": [ + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customIcon", + "columnName": "customIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "options", + "columnName": "options", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "position" + ] + }, + "indices": [], + "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, '5fa8da4798ebb9b966ac8d260c5bbee3')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index f4fa7b72..88018323 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -7,6 +7,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import de.mm20.launcher2.database.entities.* import de.mm20.launcher2.database.migrations.Migration_10_11 @@ -19,6 +20,7 @@ import de.mm20.launcher2.database.migrations.Migration_16_17 import de.mm20.launcher2.database.migrations.Migration_17_18 import de.mm20.launcher2.database.migrations.Migration_18_19 import de.mm20.launcher2.database.migrations.Migration_19_20 +import de.mm20.launcher2.database.migrations.Migration_20_21 import de.mm20.launcher2.database.migrations.Migration_6_7 import de.mm20.launcher2.database.migrations.Migration_7_8 import de.mm20.launcher2.database.migrations.Migration_8_9 @@ -34,7 +36,7 @@ import de.mm20.launcher2.database.migrations.Migration_9_10 WidgetEntity::class, CustomAttributeEntity::class, SearchActionEntity::class, - ], version = 20, exportSchema = true + ], version = 21, exportSchema = true ) @TypeConverters(ComponentNameConverter::class, StringListConverter::class) abstract class AppDatabase : RoomDatabase() { @@ -101,6 +103,7 @@ abstract class AppDatabase : RoomDatabase() { Migration_17_18(), Migration_18_19(), Migration_19_20(), + Migration_20_21(), ).build() if (_instance == null) _instance = instance return instance diff --git a/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt index b00143b6..d0cd0b27 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt @@ -5,21 +5,20 @@ import androidx.room.* import de.mm20.launcher2.database.entities.IconEntity import de.mm20.launcher2.database.entities.IconPackEntity +internal val AppTypes = listOf("app", "calendar", "clock") + @Dao interface IconDao { @Insert - fun insertAll(icons: List) + suspend fun insertAll(icons: List) - @Query("SELECT drawable FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack") - suspend fun getIconName(componentName: String, iconPack: String): String? + @Query("SELECT * FROM Icons WHERE packageName = :packageName AND (activityName = :activityName OR activityName IS NULL) AND iconPack = :iconPack AND type IN ('app', 'calendar', 'clock') LIMIT 1") + suspend fun getIcon(packageName: String, activityName: String?, iconPack: String): IconEntity? - @Query("SELECT * FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack AND (type = 'app' OR type = 'calendar') LIMIT 1") - suspend fun getIcon(componentName: String, iconPack: String): IconEntity? + @Query("SELECT * FROM Icons WHERE packageName = :packageName AND (activityName = :activityName OR activityName IS NULL) AND type IN ('app', 'calendar', 'clock')") + suspend fun getIconsFromAllPacks(packageName: String, activityName: String): List - @Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')") - suspend fun getIconsFromAllPacks(componentName: String): List - - @Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :drawableQuery OR componentName LIKE :componentQuery OR name LIKE :nameQuery) AND (:iconPack IS NULL OR iconPack = :iconPack) ORDER BY iconPack, drawable LIMIT :limit") + @Query("SELECT * FROM Icons WHERE type IN ('app', 'calendar', 'clock') AND (drawable LIKE :drawableQuery OR packageName LIKE :componentQuery OR activityName LIKE :componentQuery OR name LIKE :nameQuery) AND (:iconPack IS NULL OR iconPack = :iconPack) ORDER BY iconPack, drawable LIMIT :limit") suspend fun searchIconPackIcons( componentQuery: String, nameQuery: String, @@ -28,30 +27,10 @@ interface IconDao { limit: Int = 100 ): List - @Query("SELECT * FROM Icons WHERE (type = 'greyscale_icon') AND componentName LIKE :query GROUP BY componentName ORDER BY drawable LIMIT :limit") - suspend fun searchGreyscaleIcons(query: String, limit: Int = 100): List - - @Query("DELETE FROM Icons WHERE iconPack = :iconPack AND type != 'greyscale_icon'") + @Query("DELETE FROM Icons WHERE iconPack = :iconPack") fun deleteIcons(iconPack: String) - @Query("DELETE FROM Icons WHERE iconPack = :iconPack AND type = 'greyscale_icon'") - fun deleteGrayscaleIcons(iconPack: String) - - @Transaction - suspend fun installIconPack(iconPack: IconPackEntity, icons: List) { - deleteIconPack(iconPack) - deleteIcons(iconPack.packageName) - insertAll(icons) - installIconPack(iconPack) - } - - @Transaction - suspend fun installGrayscaleIconMap(packageName: String, icons: List) { - deleteGrayscaleIcons(packageName) - insertAll(icons) - } - - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) fun installIconPack(iconPack: IconPackEntity) @Query("SELECT * FROM IconPack") @@ -60,35 +39,9 @@ interface IconDao { @Query("SELECT * FROM IconPack WHERE packageName = :packageName LIMIT 1") suspend fun getIconPack(packageName: String): IconPackEntity? - @Query("SELECT * FROM IconPack") - fun getInstalledIconPacksLiveData(): LiveData> - @Delete fun deleteIconPack(iconPack: IconPackEntity) - @Query("SELECT * FROM IconPack WHERE packageName = :packageName AND version = :version") - suspend fun getPacks(packageName: String, version: String): List - - @Transaction - suspend fun isInstalled(iconPack: IconPackEntity): Boolean { - return getPacks(iconPack.packageName, iconPack.version).isNotEmpty() - } - - @Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs) AND (type = 'calendar' OR type = 'app')") - fun deleteAllIconsPackIconsExcept(packs: List) - - @Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs) AND type = 'greyscale_icon'") - fun deleteAllGrayscaleIconsExcept(packs: List) - - @Query("DELETE FROM IconPack WHERE packageName NOT IN (:packs)") - fun deleteAllPacksExcept(packs: List) - - @Transaction - fun uninstallIconPacksExcept(packs: List) { - deleteAllIconsPackIconsExcept(packs) - deleteAllPacksExcept(packs) - } - @Query("SELECT drawable FROM Icons WHERE iconPack = :pack AND type = 'iconback'") suspend fun getIconBacks(pack: String): List @@ -100,7 +53,4 @@ interface IconDao { @Query("SELECT scale FROM IconPack WHERE packageName = :pack") suspend fun getScale(pack: String): Float? - - @Query("SELECT * FROM Icons WHERE type = 'greyscale_icon' AND componentName = :componentName") - suspend fun getGreyscaleIcon(componentName: String): IconEntity? } \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt b/core/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt index eeb2ee99..8a065d29 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt @@ -1,16 +1,17 @@ package de.mm20.launcher2.database.entities -import android.content.ComponentName import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "Icons") data class IconEntity( val type: String, - val componentName: ComponentName?, + val packageName: String? = null, + val activityName: String? = null, val drawable: String?, + val extras: String? = null, val iconPack: String, - val name: String?, + val name: String? = null, val themed: Boolean = false, @PrimaryKey(autoGenerate = true) val id : Long? = null ) \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_20_21.kt b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_20_21.kt new file mode 100644 index 00000000..a3fdc6a7 --- /dev/null +++ b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_20_21.kt @@ -0,0 +1,23 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_20_21: Migration(20, 21) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE `Icons`") + database.execSQL("DELETE FROM `IconPack`") + database.execSQL(""" + CREATE TABLE IF NOT EXISTS `Icons` ( + `type` TEXT NOT NULL, + `packageName` TEXT, + `activityName` TEXT, + `drawable` TEXT, + `extras` TEXT, + `iconPack` TEXT NOT NULL, + `name` TEXT, + `themed` INTEGER NOT NULL DEFAULT 0, + `id` INTEGER PRIMARY KEY AUTOINCREMENT) + """) + } +} \ No newline at end of file diff --git a/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt b/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt index 2910ae6c..6bb317aa 100644 --- a/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt +++ b/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.data.customattrs +import android.content.ComponentName import android.util.Log import de.mm20.launcher2.database.entities.CustomAttributeEntity import de.mm20.launcher2.ktx.jsonObjectOf @@ -76,10 +77,20 @@ sealed class CustomIcon : CustomAttribute { val type = payload.getString("type") return when (type) { "custom_icon_pack_icon" -> { - CustomIconPackIcon( - iconComponentName = payload.getString("icon"), - iconPackPackage = payload.getString("icon_pack") - ) + val legacyComponentName = payload.optString("icon").let { ComponentName.unflattenFromString(it) } + if (legacyComponentName != null) { + CustomIconPackIcon( + iconPackageName = legacyComponentName.packageName, + iconActivityName = legacyComponentName.className, + iconPackPackage = payload.getString("icon_pack") + ) + } else { + CustomIconPackIcon( + iconPackageName = payload.optString("package").takeIf { it.isNotEmpty() } ?: return null, + iconActivityName = payload.optString("activity").takeIf { it.isNotEmpty() }, + iconPackPackage = payload.getString("icon_pack") + ) + } } "custom_themed_icon" -> { CustomThemedIcon( @@ -105,12 +116,14 @@ sealed class CustomIcon : CustomAttribute { data class CustomIconPackIcon( val iconPackPackage: String, - val iconComponentName: String, + val iconPackageName: String, + val iconActivityName: String?, ) : CustomIcon() { override fun toDatabaseValue(): String { return jsonObjectOf( "type" to "custom_icon_pack_icon", - "icon" to iconComponentName, + "package" to iconPackageName, + "activity" to iconActivityName, "icon_pack" to iconPackPackage, ).toString() } diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt index db0110f0..8a5095dd 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.icons import android.content.Context +import android.content.pm.PackageInfo import android.content.pm.ResolveInfo import de.mm20.launcher2.database.entities.IconPackEntity @@ -8,13 +9,13 @@ data class IconPack( val name: String, val packageName: String, val version: String, - var scale: Float = 1f, + val scale: Float = 1f, val themed: Boolean = false, ) { constructor(entity: IconPackEntity) : this( name = entity.name, packageName = entity.packageName, - version = entity.packageName, + version = entity.version, scale = entity.scale, themed = entity.themed, ) @@ -30,6 +31,17 @@ data class IconPack( themed = themed, ) + internal constructor( + context: Context, + packageInfo: PackageInfo, + themed: Boolean = false + ): this( + name = packageInfo.applicationInfo.loadLabel(context.packageManager).toString(), + packageName = packageInfo.packageName, + version = context.packageManager.getPackageInfo(packageInfo.packageName, 0).versionName, + themed = themed, + ) + fun toDatabaseEntity(): IconPackEntity { return IconPackEntity( name = name, diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackIcon.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackIcon.kt index 7f178559..49d11c07 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackIcon.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackIcon.kt @@ -1,33 +1,188 @@ package de.mm20.launcher2.icons -import android.content.ComponentName import de.mm20.launcher2.database.entities.IconEntity +import de.mm20.launcher2.icons.compat.ClockIconConfig +import de.mm20.launcher2.ktx.jsonObjectOf +import org.json.JSONObject -data class IconPackIcon( - val type: String, - val componentName: ComponentName?, - val drawable: String?, - val iconPack: String, - val themed: Boolean = false, - val name: String? = null, -) { - constructor(entity: IconEntity) : this( - type = entity.type, - componentName = entity.componentName, - drawable = entity.drawable, - iconPack = entity.iconPack, - name = entity.name, - themed = entity.themed, - ) +sealed interface IconPackComponent { + val iconPack: String + fun toDatabaseEntity(): IconEntity +} - fun toDatabaseEntity(): IconEntity { +sealed interface IconPackAppIcon: IconPackComponent { + val packageName: String + val activityName: String? + val name: String? + val themed: Boolean +} + +data class IconBack( + val drawable: String, + override val iconPack: String, +): IconPackComponent { + override fun toDatabaseEntity(): IconEntity { return IconEntity( - type = type, - componentName = componentName, + type = "iconback", drawable = drawable, iconPack = iconPack, + ) + } +} + +data class IconUpon( + val drawable: String, + override val iconPack: String, +): IconPackComponent { + override fun toDatabaseEntity(): IconEntity { + return IconEntity( + type = "iconupon", + drawable = drawable, + iconPack = iconPack, + ) + } +} + +data class IconMask( + val drawable: String, + override val iconPack: String, +): IconPackComponent { + override fun toDatabaseEntity(): IconEntity { + return IconEntity( + type = "iconmask", + drawable = drawable, + iconPack = iconPack, + ) + } +} + +data class AppIcon( + val drawable: String, + override val iconPack: String, + override val packageName: String, + override val activityName: String?, + override val name: String?, + override val themed: Boolean = false, +): IconPackComponent, IconPackAppIcon { + override fun toDatabaseEntity(): IconEntity { + return IconEntity( + type = "app", + packageName = packageName, + activityName = activityName, + drawable = drawable, + name = name, + iconPack = iconPack, + themed = themed, + ) + } +} + +data class CalendarIcon( + val drawables: List, + override val iconPack: String, + override val packageName: String, + override val activityName: String? = null, + override val name: String? = null, + override val themed: Boolean = false, +): IconPackComponent, IconPackAppIcon { + override fun toDatabaseEntity(): IconEntity { + return IconEntity( + type = "calendar", + drawable = drawables.joinToString(","), + iconPack = iconPack, + packageName = packageName, + activityName = activityName, name = name, themed = themed, ) } -} \ No newline at end of file +} + + +data class ClockIcon( + val drawable: String, + override val iconPack: String, + override val packageName: String, + override val activityName: String? = null, + override val name: String? = null, + override val themed: Boolean, + val config: ClockIconConfig, +): IconPackComponent, IconPackAppIcon { + override fun toDatabaseEntity(): IconEntity { + return IconEntity( + type = "clock", + packageName = packageName, + activityName = activityName, + drawable = drawable, + name = name, + iconPack = iconPack, + themed = themed, + extras = jsonObjectOf( + "defaultSecond" to config.defaultSecond, + "defaultMinute" to config.defaultMinute, + "defaultHour" to config.defaultHour, + "hourLayer" to config.hourLayer, + "minuteLayer" to config.minuteLayer, + "secondLayer" to config.secondLayer, + ).toString(), + ) + } +} + +fun Icon(entity: IconEntity): IconPackComponent? { + return when(entity.type) { + "iconback" -> IconBack( + drawable = entity.drawable ?: return null, + iconPack = entity.iconPack, + ) + "iconupon" -> IconUpon( + drawable = entity.drawable ?: return null, + iconPack = entity.iconPack, + ) + "iconmask" -> IconMask( + drawable = entity.drawable ?: return null, + iconPack = entity.iconPack, + ) + "app" -> AppIcon( + drawable = entity.drawable ?: return null, + iconPack = entity.iconPack, + packageName = entity.packageName ?: return null, + activityName = entity.activityName, + themed = entity.themed, + name = entity.name, + ) + "calendar" -> CalendarIcon( + drawables = entity.drawable?.split(",") ?: return null, + iconPack = entity.iconPack, + themed = entity.themed, + packageName = entity.packageName ?: return null, + activityName = entity.activityName, + name = entity.name, + ) + "clock" -> { + val config = JSONObject(entity.extras ?: return null) + ClockIcon( + drawable = entity.drawable!!, + iconPack = entity.iconPack, + packageName = entity.packageName ?: return null, + name = entity.name, + activityName = entity.activityName, + themed = entity.themed, + config = ClockIconConfig( + defaultSecond = config.optInt("defaultSecond", 0), + defaultMinute = config.optInt("defaultMinute", 0), + defaultHour = config.optInt("defaultHour", 0), + hourLayer = config.optInt("hourLayer", 0), + minuteLayer = config.optInt("minuteLayer", 0), + secondLayer = config.optInt("secondLayer", 0), + ) + ) + } + else -> null + } +} + +fun IconPackAppIcon(entity: IconEntity): IconPackAppIcon? { + if (entity.type != "app" && entity.type != "calendar" && entity.type != "clock") return null + return Icon(entity) as? IconPackAppIcon +} diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt index ca4fd4d5..ca395b38 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt @@ -18,14 +18,14 @@ import android.graphics.drawable.RotateDrawable import android.util.Log import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap -import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase -import de.mm20.launcher2.icons.loaders.GrayscaleMapInstaller -import de.mm20.launcher2.icons.loaders.IconPackInstaller +import de.mm20.launcher2.icons.loaders.AppFilterIconPackInstaller +import de.mm20.launcher2.icons.loaders.GrayscaleMapIconPackInstaller import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.ktx.randomElementOrNull import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext import kotlin.math.roundToInt @@ -50,16 +50,33 @@ class IconPackManager( } } - suspend fun updateIconPacks() { - withContext(Dispatchers.IO) { - IconPackInstaller(context, appDatabase).installIcons() - GrayscaleMapInstaller(context, appDatabase).installIcons() + private var updateIconPacksMutex = Mutex() + suspend fun updateIconPacks(): Boolean { + var iconsHaveBeenUpdated = false + updateIconPacksMutex.lock() + val installers = listOf( + AppFilterIconPackInstaller(context, appDatabase), + GrayscaleMapIconPackInstaller(context, appDatabase), + ) + for (installer in installers) { + val iconPacks = installer.getInstalledIconPacks() + for (pack in iconPacks) { + if (!installer.isInstalledAndUpToDate(pack)) { + installer.install(pack) + iconsHaveBeenUpdated = true + } else { + Log.d("MM20", "Icon pack ${pack.packageName} is up to date") + } + } } + updateIconPacksMutex.unlock() + return iconsHaveBeenUpdated } suspend fun getIcon( iconPack: String, - componentName: ComponentName, + packageName: String, + activityName: String?, ): LauncherIcon? { val res = try { context.packageManager.getResourcesForApplication(iconPack) @@ -68,69 +85,17 @@ class IconPackManager( return null } val iconDao = appDatabase.iconDao() - val icon = iconDao.getIcon(componentName.flattenToString(), iconPack) + val icon = iconDao.getIcon(packageName, activityName, iconPack)?.let { IconPackAppIcon(it) } ?: return null - val drawableName = icon.drawable ?: return null - - if (icon.type == "calendar") { - return getIconPackCalendarIcon(context, iconPack, drawableName, icon.themed) - } - val resId = res.getIdentifier(drawableName, "drawable", iconPack).takeIf { it != 0 } - ?: return null - val drawable = try { - ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null - } catch (e: Resources.NotFoundException) { - return null - } - return when { - icon.themed && drawable is AdaptiveIconDrawable -> { - if (isAtLeastApiLevel(33) && drawable.monochrome != null) { - return StaticLauncherIcon( - foregroundLayer = StaticIconLayer( - icon = drawable.monochrome!!, - scale = 1f, - ), - backgroundLayer = ColorLayer(), - ) - } else { - return StaticLauncherIcon( - foregroundLayer = TintedIconLayer( - icon = drawable.foreground, - scale = 1.5f, - ), - backgroundLayer = ColorLayer(), - ) - } - } - - drawable is AdaptiveIconDrawable -> { - return StaticLauncherIcon( - foregroundLayer = drawable.foreground?.let { - StaticIconLayer( - icon = it, - scale = 1.5f, - ) - } ?: TransparentLayer, - backgroundLayer = drawable.background?.let { - StaticIconLayer( - icon = it, - scale = 1.5f, - ) - } ?: TransparentLayer, - ) - } - - else -> { - StaticLauncherIcon( - foregroundLayer = StaticIconLayer( - icon = drawable, - scale = 1f - ), - backgroundLayer = TransparentLayer - ) - } + if (icon is CalendarIcon) { + return getIconPackCalendarIcon(icon, res) + } else if (icon is AppIcon) { + return getIconPackStaticIcon(icon, res) + } else if (icon is ClockIcon) { + return getIconPackClockIcon(icon, res) } + return null } suspend fun generateIcon( @@ -233,10 +198,10 @@ class IconPackManager( ) } - suspend fun getAllIconPackIcons(componentName: ComponentName): List { + suspend fun getAllIconPackIcons(componentName: ComponentName): List { val iconDao = appDatabase.iconDao() - return iconDao.getIconsFromAllPacks(componentName.flattenToString()) - .map { IconPackIcon(it) } + return iconDao.getIconsFromAllPacks(componentName.packageName, componentName.shortClassName) + .mapNotNull { IconPackAppIcon(it) } } private suspend fun getIconBack(iconPack: String): String? { @@ -262,25 +227,90 @@ class IconPackManager( return iconDao.getScale(iconPack) ?: 1f } - private fun getIconPackCalendarIcon( - context: Context, - iconPack: String, - baseIconName: String, - themed: Boolean, + private fun getIconPackStaticIcon( + icon: AppIcon, + resources: Resources, ): LauncherIcon? { - val resources = try { - context.packageManager.getResourcesForApplication(iconPack) - } catch (e: PackageManager.NameNotFoundException) { + val resId = + resources.getIdentifier(icon.drawable, "drawable", icon.iconPack).takeIf { it != 0 } + ?: return null + val drawable = try { + ResourcesCompat.getDrawable(resources, resId, context.theme) ?: return null + } catch (e: Resources.NotFoundException) { return null } - val drawableIds = (1..31).map { - val drawableName = baseIconName + it - val id = resources.getIdentifier(drawableName, "drawable", iconPack) + return when { + icon.themed && drawable is AdaptiveIconDrawable -> { + if (isAtLeastApiLevel(33) && drawable.monochrome != null) { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + icon = drawable.monochrome!!, + scale = 1f, + ), + backgroundLayer = ColorLayer(), + ) + } else { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + icon = drawable.foreground, + scale = 1.5f, + ), + backgroundLayer = ColorLayer(), + ) + } + } + + icon.themed -> { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + icon = drawable, + scale = 0.5f, + ), + backgroundLayer = ColorLayer(), + ) + } + + drawable is AdaptiveIconDrawable -> { + return StaticLauncherIcon( + foregroundLayer = drawable.foreground?.let { + StaticIconLayer( + icon = it, + scale = 1.5f, + ) + } ?: TransparentLayer, + backgroundLayer = drawable.background?.let { + StaticIconLayer( + icon = it, + scale = 1.5f, + ) + } ?: TransparentLayer, + ) + } + + else -> { + StaticLauncherIcon( + foregroundLayer = StaticIconLayer( + icon = drawable, + scale = 1f + ), + backgroundLayer = TransparentLayer + ) + } + } + } + + private fun getIconPackCalendarIcon( + icon: CalendarIcon, + resources: Resources, + ): LauncherIcon? { + val drawableIds = icon.drawables.map { + val id = resources.getIdentifier(it, "drawable", icon.iconPack) if (id == 0) return null id }.toIntArray() - if (themed) { + + if (icon.themed) { return ThemedDynamicCalendarIcon( resources = resources, resourceIds = drawableIds, @@ -292,142 +322,91 @@ class IconPackManager( ) } - suspend fun getThemedIcon(packageName: String): LauncherIcon? { - val icon = getGreyscaleIcon(packageName) ?: return null - val resId = icon.drawable?.toIntOrNull() ?: return null - try { - val resources = context.packageManager.getResourcesForApplication(icon.iconPack) - return getThemedClockIcon(resources, resId) ?: getThemedCalendarIcon( - resources, - resId, - iconProviderPackage = icon.iconPack - ) ?: getThemedStaticIcon(resources, resId) - } catch (e: PackageManager.NameNotFoundException) { - CrashReporter.logException(e) - } - return null - } - - - suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? { - val iconDao = AppDatabase.getInstance(context).iconDao() - return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString()) - ?.let { IconPackIcon(it) } - - } - - private fun getThemedStaticIcon(resources: Resources, resId: Int): LauncherIcon? { - try { - val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null - return StaticLauncherIcon( - foregroundLayer = TintedIconLayer( - icon = fg, - scale = 0.5f, - ), - backgroundLayer = ColorLayer() - ) + private fun getIconPackClockIcon( + icon: ClockIcon, + resources: Resources, + ): LauncherIcon? { + var drawable = try { + resources.getIdentifier(icon.drawable, "drawable", icon.iconPack).takeIf { it != 0 } + ?.let { ResourcesCompat.getDrawable(resources, it, null) } } catch (e: Resources.NotFoundException) { - return null - } - } + null + } ?: return null - private fun getThemedClockIcon(resources: Resources, resId: Int): LauncherIcon? { - try { - val array = resources.obtainTypedArrayOrNull(resId) ?: return null - var i = 0 - var drawable: LayerDrawable? = null - var minuteIndex: Int? = null - var defaultMinute = 0 - var hourIndex: Int? = null - var defaultHour = 0 - while (i < array.length()) { - when (array.getString(i)) { - "com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> { - i++ - drawable = array.getDrawable(i) as? LayerDrawable - } + val background = (drawable as? AdaptiveIconDrawable)?.background + val foreground = (drawable as? AdaptiveIconDrawable)?.foreground ?: drawable - "com.android.launcher3.HOUR_LAYER_INDEX" -> { - i++ - hourIndex = array.getInt(i, -1).takeIf { it != -1 } - } + if (foreground !is LayerDrawable) return null - "com.android.launcher3.MINUTE_LAYER_INDEX" -> { - i++ - minuteIndex = array.getInt(i, -1).takeIf { it != -1 } - } - - "com.android.launcher3.DEFAULT_HOUR" -> { - i++ - defaultHour = array.getInt(i, 0) - } - - "com.android.launcher3.DEFAULT_MINUTE" -> { - i++ - defaultMinute = array.getInt(i, 0) - } + val layers = (0 until foreground.numberOfLayers).map { + val drw = foreground.getDrawable(it) + ClockSublayer( + drawable = drw, + role = when (it) { + icon.config.hourLayer -> ClockSublayerRole.Hour + icon.config.minuteLayer -> ClockSublayerRole.Minute + icon.config.secondLayer -> ClockSublayerRole.Second + else -> ClockSublayerRole.Static } - i++ - } - if (drawable != null && minuteIndex != null && hourIndex != null) { + ) + } - return StaticLauncherIcon( + return when { + icon.themed && drawable is AdaptiveIconDrawable -> { + StaticLauncherIcon( foregroundLayer = TintedClockLayer( - sublayers = (0 until drawable.numberOfLayers).map { - val drw = drawable.getDrawable(it) - if (drw is RotateDrawable) { - drw.level = when (it) { - hourIndex -> { - (12 - defaultHour) * 60 - } - - minuteIndex -> { - (60 - defaultMinute) - } - - else -> 0 - } - } - ClockSublayer( - drawable = drw, - role = when (it) { - hourIndex -> ClockSublayerRole.Hour - minuteIndex -> ClockSublayerRole.Minute - else -> ClockSublayerRole.Static - } - ) - }, + defaultHour = icon.config.defaultHour, + defaultMinute = icon.config.defaultMinute, + defaultSecond = icon.config.defaultSecond, + sublayers = layers, scale = 1.5f, ), - backgroundLayer = ColorLayer() + backgroundLayer = ColorLayer(), + ) + } + icon.themed -> { + StaticLauncherIcon( + foregroundLayer = TintedClockLayer( + defaultHour = icon.config.defaultHour, + defaultMinute = icon.config.defaultMinute, + defaultSecond = icon.config.defaultSecond, + sublayers = layers, + scale = 1f, + ), + backgroundLayer = ColorLayer(), + ) + } + drawable is AdaptiveIconDrawable -> { + StaticLauncherIcon( + foregroundLayer = ClockLayer( + defaultHour = icon.config.defaultHour, + defaultMinute = icon.config.defaultMinute, + defaultSecond = icon.config.defaultSecond, + sublayers = layers, + scale = 1.5f, + ), + backgroundLayer = StaticIconLayer( + icon = background!!, + scale = 1.5f + ), + ) + } + else -> { + StaticLauncherIcon( + foregroundLayer = ClockLayer( + defaultHour = icon.config.defaultHour, + defaultMinute = icon.config.defaultMinute, + defaultSecond = icon.config.defaultSecond, + sublayers = layers, + scale = 1f, + ), + backgroundLayer = TransparentLayer, ) } - } catch (e: Resources.NotFoundException) { } - return null } - private fun getThemedCalendarIcon( - resources: Resources, - resId: Int, - iconProviderPackage: String - ): LauncherIcon? { - try { - val array = resources.obtainTypedArrayOrNull(resId) ?: return null - if (array.length() != 31) return null - - return ThemedDynamicCalendarIcon( - resources = resources, - resourceIds = IntArray(31) { - array.getResourceId(it, 0).takeIf { it != 0 } ?: return null - }, - ) - } catch (e: Resources.NotFoundException) { - } - return null - } - - suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List { + suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List { val iconDao = appDatabase.iconDao() val drawableQuery = query.replace(" ", "_").lowercase() return iconDao.searchIconPackIcons( @@ -435,17 +414,11 @@ class IconPackManager( componentQuery = "%$query%", nameQuery = "%$query%", iconPack = iconPack?.packageName, - ).map { - IconPackIcon(it) + ).mapNotNull { + IconPackAppIcon(it) } } - suspend fun searchThemedIcons(query: String): List { - val iconDao = appDatabase.iconDao() - return iconDao.searchGreyscaleIcons("%$query%").map { - IconPackIcon(it) - } - } } diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt index 36c09b66..9bf3b004 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.icons import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -24,7 +25,6 @@ import de.mm20.launcher2.icons.providers.IconPackIconProvider import de.mm20.launcher2.icons.providers.IconProvider import de.mm20.launcher2.icons.providers.PlaceholderIconProvider import de.mm20.launcher2.icons.providers.SystemIconProvider -import de.mm20.launcher2.icons.providers.ThemedIconProvider import de.mm20.launcher2.icons.providers.ThemedPlaceholderIconProvider import de.mm20.launcher2.icons.providers.getFirstIcon import de.mm20.launcher2.icons.transformations.ForceThemedIconTransformation @@ -39,11 +39,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.internal.ChannelFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -67,6 +69,11 @@ class IconRepository( private var iconProviders: MutableStateFlow> = MutableStateFlow(listOf()) private var placeholderProvider: IconProvider? = null + /** + * Signal that installed icon packs have been updated. Force a reload of all icons. + */ + private val iconPacksUpdated = MutableSharedFlow(1) + private var transformations: MutableStateFlow> = MutableStateFlow( listOf() @@ -83,51 +90,52 @@ class IconRepository( addDataScheme("package") }) + iconPacksUpdated.tryEmit(Unit) + scope.launch { dataStore.data.map { it.icons }.distinctUntilChanged().collectLatest { settings -> - val placeholderProvider = if (settings.themedIcons) { - ThemedPlaceholderIconProvider(context) - } else { - PlaceholderIconProvider(context) - } - val providers = mutableListOf() - - if (settings.iconPack.isNotBlank()) { - val pack = iconPackManager.getIconPack(settings.iconPack) - if (pack != null) { - providers.add( - IconPackIconProvider( - context, - pack, - iconPackManager - ) - ) + iconPacksUpdated.collectLatest { + val placeholderProvider = if (settings.themedIcons) { + ThemedPlaceholderIconProvider(context) } else { - Log.w("MM20", "Icon pack ${settings.iconPack} not found") + PlaceholderIconProvider(context) } - } - if (settings.themedIcons) { - providers.add(ThemedIconProvider(iconPackManager)) - } - providers.add(DynamicClockIconProvider(context, settings.themedIcons)) - providers.add(CalendarIconProvider(context, settings.themedIcons)) - if (!isAtLeastApiLevel(33)) { - providers.add(CompatIconProvider(context, settings.themedIcons)) - } - providers.add(SystemIconProvider(context, settings.themedIcons)) - providers.add(placeholderProvider) - cache.evictAll() + val providers = mutableListOf() - val transformations = mutableListOf() + if (settings.iconPack.isNotBlank()) { + val pack = iconPackManager.getIconPack(settings.iconPack) + if (pack != null) { + providers.add( + IconPackIconProvider( + context, + pack, + iconPackManager + ) + ) + } else { + Log.w("MM20", "Icon pack ${settings.iconPack} not found") + } + } + providers.add(DynamicClockIconProvider(context, settings.themedIcons)) + providers.add(CalendarIconProvider(context, settings.themedIcons)) + if (!isAtLeastApiLevel(33)) { + providers.add(CompatIconProvider(context, settings.themedIcons)) + } + providers.add(SystemIconProvider(context, settings.themedIcons)) + providers.add(placeholderProvider) + cache.evictAll() - if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation()) - if (settings.themedIcons && settings.forceThemed) transformations.add( - ForceThemedIconTransformation() - ) + val transformations = mutableListOf() - this@IconRepository.placeholderProvider = placeholderProvider - iconProviders.value = providers - this@IconRepository.transformations.value = transformations + if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation()) + if (settings.themedIcons && settings.forceThemed) transformations.add( + ForceThemedIconTransformation() + ) + + this@IconRepository.placeholderProvider = placeholderProvider + iconProviders.value = providers + this@IconRepository.transformations.value = transformations + } } } } @@ -215,7 +223,9 @@ class IconRepository( fun requestIconPackListUpdate() { scope.launch { - iconPackManager.updateIconPacks() + iconPackManager.updateIconPacks().also { + if (it)iconPacksUpdated.tryEmit(Unit) + } } } @@ -285,24 +295,14 @@ class IconRepository( iconPackIcons.mapNotNull { CustomIconPackIcon( iconPackPackage = it.iconPack, - iconComponentName = it.componentName?.flattenToString() - ?: return@mapNotNull null + iconActivityName = it.activityName, + iconPackageName = it.packageName, ) } ) - - val themedIcon = iconPackManager.getGreyscaleIcon(searchable.`package`) - if (themedIcon != null && themedIcon.componentName?.packageName != null) { - providerOptions.add( - CustomThemedIcon( - iconPackageName = themedIcon.componentName.packageName, - ) - ) - } else { - transformationOptions.add( - ForceThemedIcon - ) - } + transformationOptions.add( + ForceThemedIcon + ) } else { transformationOptions.add( ForceThemedIcon @@ -359,31 +359,18 @@ class IconRepository( suspend fun searchCustomIcons(query: String, iconPack: IconPack?): List { val transformations = this.transformations.first() val iconPackIcons = iconPackManager.searchIconPackIcon(query, iconPack).mapNotNull { - val componentName = it.componentName ?: return@mapNotNull null - CustomIconWithPreview( customIcon = CustomIconPackIcon( iconPackPackage = it.iconPack, - iconComponentName = componentName.flattenToString(), + iconActivityName = it.activityName, + iconPackageName = it.packageName, ), - preview = iconPackManager.getIcon(it.iconPack, componentName) + preview = iconPackManager.getIcon(it.iconPack, it.packageName, it.activityName) ?.transform(transformations) ?: return@mapNotNull null ) } - val themedIcons = iconPackManager.searchThemedIcons(query).mapNotNull { - val componentName = it.componentName ?: return@mapNotNull null - - CustomIconWithPreview( - customIcon = CustomThemedIcon( - iconPackageName = componentName.packageName, - ), - preview = iconPackManager.getThemedIcon(componentName.packageName) - ?.transform(transformations) ?: return@mapNotNull null - ) - } - - return iconPackIcons + themedIcons + return iconPackIcons } fun setCustomIcon(searchable: SavableSearchable, icon: CustomIcon?) { diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/compat/AdaptiveIconDrawableCompat.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/compat/AdaptiveIconDrawableCompat.kt index 243ddfac..8394a37e 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/compat/AdaptiveIconDrawableCompat.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/compat/AdaptiveIconDrawableCompat.kt @@ -145,23 +145,6 @@ fun AdaptiveIconDrawableCompat.toLauncherIcon( if (clock != null && clockForeground != null) { val clockLayers = (0 until clockForeground.numberOfLayers).map { val drw = clockForeground.getDrawable(it) - if (drw is RotateDrawable) { - drw.level = when (it) { - clock.hourLayer -> { - (12 - clock.defaultHour) * 60 - } - - clock.minuteLayer -> { - (60 - clock.defaultMinute) - } - - clock.secondLayer -> { - (60 - clock.defaultSecond) * 10 - } - - else -> 0 - } - } ClockSublayer( drawable = drw, role = when (it) { @@ -175,6 +158,9 @@ fun AdaptiveIconDrawableCompat.toLauncherIcon( if (themed) { return StaticLauncherIcon( foregroundLayer = TintedClockLayer( + defaultHour = clock.defaultHour, + defaultMinute = clock.defaultMinute, + defaultSecond = clock.defaultSecond, sublayers = clockLayers, scale = 1.5f, ), @@ -183,6 +169,9 @@ fun AdaptiveIconDrawableCompat.toLauncherIcon( } return StaticLauncherIcon( foregroundLayer = ClockLayer( + defaultHour = clock.defaultHour, + defaultMinute = clock.defaultMinute, + defaultSecond = clock.defaultSecond, sublayers = clockLayers, scale = 1.5f, ), diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/AppFilterIconPackInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/AppFilterIconPackInstaller.kt new file mode 100644 index 00000000..62c0078c --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/AppFilterIconPackInstaller.kt @@ -0,0 +1,197 @@ +package de.mm20.launcher2.icons.loaders + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.XmlResourceParser +import android.util.Log +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.database.AppDatabase +import de.mm20.launcher2.icons.AppIcon +import de.mm20.launcher2.icons.CalendarIcon +import de.mm20.launcher2.icons.IconBack +import de.mm20.launcher2.icons.IconMask +import de.mm20.launcher2.icons.IconPack +import de.mm20.launcher2.icons.IconUpon +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.IOException +import java.io.Reader + +class AppFilterIconPackInstaller( + private val context: Context, + database: AppDatabase, +) : IconPackInstaller(database) { + override suspend fun IconPackInstallerScope.buildIconPack(iconPack: IconPack) { + withContext(Dispatchers.IO) { + val pkgName = iconPack.packageName + + try { + val res = context.packageManager.getResourcesForApplication(pkgName) + val parser: XmlPullParser + var inStream: Reader? = null + val xmlId = res.getIdentifier("appfilter", "xml", pkgName) + val rawId = res.getIdentifier("appfilter", "raw", pkgName) + parser = when { + xmlId != 0 -> res.getXml(xmlId) + rawId != 0 -> { + inStream = res.openRawResource(rawId).reader() + XmlPullParserFactory.newInstance().newPullParser().apply { + setInput(inStream) + } + } + + else -> { + val iconPackContext = context.createPackageContext( + pkgName, + Context.CONTEXT_IGNORE_SECURITY + ) + inStream = try { + iconPackContext.assets.open("appfilter.xml").reader() + } catch (e: IOException) { + CrashReporter.logException(e) + Log.e( + "MM20", + "appfilter.xml not found in $pkgName. Searched locations: res/xml/appfilter.xml, res/raw/appfilter.xml, assets/appfilter.xml" + ) + return@withContext + } + XmlPullParserFactory.newInstance().newPullParser().apply { + setInput(inStream) + } + } + } + + loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType != XmlPullParser.START_TAG) continue + when (parser.name) { + "item" -> { + val component = parser.getAttributeValue(null, "component") + ?: continue@loop + val drawable = parser.getAttributeValue(null, "drawable") + ?: continue@loop + if (component.length <= 14) continue@loop + val componentName = ComponentName.unflattenFromString( + component.substring( + 14, + component.lastIndex + ) + ) + ?: continue@loop + + val name = parser.getAttributeValue(null, "name") + + val icon = AppIcon( + packageName = componentName.packageName, + activityName = componentName.shortClassName, + drawable = drawable, + iconPack = pkgName, + name = name, + themed = iconPack.themed, + ) + addIcon(icon) + } + + "calendar" -> { + val component = parser.getAttributeValue(null, "component") + ?: continue@loop + val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop + if (component.length < 14) continue@loop + val componentName = ComponentName.unflattenFromString( + component.substring( + 14, + component.lastIndex + ) + ) + ?: continue@loop + + val name = parser.getAttributeValue(null, "name") + + val icon = CalendarIcon( + packageName = componentName.packageName, + activityName = componentName.shortClassName, + drawables = (0..31).map { "$drawable$it" }, + iconPack = pkgName, + themed = iconPack.themed, + name = name, + ) + addIcon(icon) + } + + "iconback" -> { + for (i in 0 until parser.attributeCount) { + if (parser.getAttributeName(i).startsWith("img")) { + val drawable = parser.getAttributeValue(i) + val icon = IconBack( + drawable = drawable, + iconPack = pkgName, + ) + addIcon(icon) + } + } + } + + "iconupon" -> { + for (i in 0 until parser.attributeCount) { + if (parser.getAttributeName(i).startsWith("img")) { + val drawable = parser.getAttributeValue(i) + val icon = IconUpon( + drawable = drawable, + iconPack = pkgName, + ) + addIcon(icon) + } + } + } + + "iconmask" -> { + for (i in 0 until parser.attributeCount) { + if (parser.getAttributeName(i).startsWith("img")) { + val drawable = parser.getAttributeValue(i) + val icon = IconMask( + drawable = drawable, + iconPack = pkgName, + ) + addIcon(icon) + } + } + } + + "scale" -> { + val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull() + ?: continue@loop + updatePackInfo { it.copy(scale = scale) } + } + } + } + (parser as? XmlResourceParser)?.close() + inStream?.close() + + Log.d("MM20", "Icon pack $pkgName has been installed successfully") + } catch (e: PackageManager.NameNotFoundException) { + Log.e("MM20", "Could not install icon pack $pkgName: package not found.") + } catch (e: XmlPullParserException) { + CrashReporter.logException(e) + } + } + } + + override fun getInstalledIconPacks(): List { + val packs = mutableListOf() + val pm = context.packageManager + var intent = Intent("app.lawnchair.icons.THEMED_ICON") + val themedPacks = pm.queryIntentActivities(intent, 0) + packs.addAll(themedPacks.map { IconPack(context, it, true) }) + intent = Intent("org.adw.ActivityStarter.THEMES") + val adwPacks = pm.queryIntentActivities(intent, 0) + packs.addAll(adwPacks.map { IconPack(context, it, false) }) + intent = Intent("com.novalauncher.THEME") + val novaPacks = pm.queryIntentActivities(intent, 0) + packs.addAll(novaPacks.map { IconPack(context, it, false) }) + return packs.distinctBy { it.packageName } + } +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapIconPackInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapIconPackInstaller.kt new file mode 100644 index 00000000..a3f78946 --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapIconPackInstaller.kt @@ -0,0 +1,178 @@ +package de.mm20.launcher2.icons.loaders + +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.drawable.LayerDrawable +import android.util.Log +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.database.AppDatabase +import de.mm20.launcher2.icons.AppIcon +import de.mm20.launcher2.icons.CalendarIcon +import de.mm20.launcher2.icons.ClockIcon +import de.mm20.launcher2.icons.IconPack +import de.mm20.launcher2.icons.compat.ClockIconConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xmlpull.v1.XmlPullParser + +class GrayscaleMapIconPackInstaller( + private val context: Context, + database: AppDatabase, +) : IconPackInstaller(database) { + private val SUPPORTED_GRAYSCALE_MAP_PROVIDERS = arrayOf( + "app.lawnchair.lawnicons", // Lawnicons + "de.mm20.launcher2.themedicons", + "de.kvaesitso.icons", + ) + + override suspend fun IconPackInstallerScope.buildIconPack(iconPack: IconPack) { + withContext(Dispatchers.IO) { + try { + val packageName = iconPack.packageName + val resources = context.packageManager.getResourcesForApplication(packageName) + val resId = resources.getIdentifier("grayscale_icon_map", "xml", packageName) + if (resId == 0) { + Log.d("MM20", "Could not find grayscale_icon_map.xml in $packageName") + return@withContext + } + val parser = resources.getXml(resId) + loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType != XmlPullParser.START_TAG) continue + if (parser.name == "icon") { + val pkg = parser.getAttributeValue(null, "package") ?: continue@loop + val drawableRes = + parser.getAttributeResourceValue(null, "drawable", 0) + + val type = try { + resources.getResourceTypeName(drawableRes) + } catch (e: Resources.NotFoundException) { + continue@loop + } + + if (type == "drawable") { + val drawableName = + resources.getResourceEntryNameOrNull(drawableRes) ?: continue@loop + val icon = AppIcon( + drawable = drawableName, + packageName = pkg, + activityName = null, + iconPack = packageName, + name = null, + themed = true + ) + addIcon(icon) + } else if (type == "array") { + val array = resources.obtainTypedArray(drawableRes) + if (array.length() == 31) { + val drawables = mutableListOf() + for (i in 0 until 31) { + val res = array.getResourceId(i, 0) + if (res == 0) break + val drawableName = + resources.getResourceEntryNameOrNull(res) ?: break + drawables.add(drawableName) + } + if (drawables.size == 31) { + addIcon( + CalendarIcon( + drawables = drawables, + packageName = pkg, + iconPack = packageName, + themed = true + ) + ) + } + } else { + var i = 0 + var drawable: LayerDrawable? = null + var drawableName: String? = null + var minuteIndex: Int? = null + var defaultMinute = 0 + var hourIndex: Int? = null + var defaultHour = 0 + while (i < array.length()) { + when (array.getString(i)) { + "com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> { + i++ + drawable = array.getDrawable(i) as? LayerDrawable + drawableName = + array.getResourceId(i, 0).takeIf { it != 0 } + ?.let { resources.getResourceEntryNameOrNull(it) } + } + + "com.android.launcher3.HOUR_LAYER_INDEX" -> { + i++ + hourIndex = array.getInt(i, -1).takeIf { it != -1 } + } + + "com.android.launcher3.MINUTE_LAYER_INDEX" -> { + i++ + minuteIndex = array.getInt(i, -1).takeIf { it != -1 } + } + + "com.android.launcher3.DEFAULT_HOUR" -> { + i++ + defaultHour = array.getInt(i, 0) + } + + "com.android.launcher3.DEFAULT_MINUTE" -> { + i++ + defaultMinute = array.getInt(i, 0) + } + } + i++ + } + if (drawable != null && drawableName != null && minuteIndex != null && hourIndex != null) { + addIcon( + ClockIcon( + drawable = drawableName, + packageName = pkg, + config = ClockIconConfig( + hourLayer = hourIndex, + minuteLayer = minuteIndex, + defaultHour = defaultHour, + defaultMinute = defaultMinute, + defaultSecond = 0, + secondLayer = -1, + ), + iconPack = packageName, + themed = true, + ) + ) + } + } + array.recycle() + } + } + } + } catch (e: PackageManager.NameNotFoundException) { + CrashReporter.logException(e) + } + } + } + + override fun getInstalledIconPacks(): List { + val pm = context.packageManager + return SUPPORTED_GRAYSCALE_MAP_PROVIDERS.mapNotNull { + try { + val packageInfo = pm.getPackageInfo(it, 0) + IconPack( + context = context, + packageInfo = packageInfo, + themed = true, + ) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + } +} + +internal fun Resources.getResourceEntryNameOrNull(res: Int): String? { + return try { + getResourceEntryName(res) + } catch (e: Resources.NotFoundException) { + null + } +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt deleted file mode 100644 index 987894f2..00000000 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt +++ /dev/null @@ -1,88 +0,0 @@ -package de.mm20.launcher2.icons.loaders - -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import de.mm20.launcher2.database.AppDatabase -import de.mm20.launcher2.icons.IconPackIcon -import org.xmlpull.v1.XmlPullParser - -class GrayscaleMapInstaller( - private val context: Context, - private val database: AppDatabase, -) { - private val SUPPORTED_GRAYSCALE_MAP_PROVIDERS = arrayOf( - "com.google.android.apps.nexuslauncher", // Pixel Launcher - "app.lawnchair.lawnicons", // Lawnicons - "app.lawnchair", // Lawnchair - "de.mm20.launcher2.themedicons", - "de.kvaesitso.icons", - ) - - fun installIcons() { - val grayscaleProviders = loadInstalledGreyscaleProviders(context) - - val dao = database.iconDao() - - grayscaleProviders.forEach { installGrayscaleIconMap(it) } - - dao.deleteAllGrayscaleIconsExcept(grayscaleProviders) - } - - private fun loadInstalledGreyscaleProviders(context: Context): List { - val pm = context.packageManager - return SUPPORTED_GRAYSCALE_MAP_PROVIDERS.filter { - try { - pm.getPackageInfo(it, 0) - true - } catch (e: PackageManager.NameNotFoundException) { - false - } - } - } - - private fun installGrayscaleIconMap(packageName: String) { - database.runInTransaction { - val iconDao = database.iconDao() - try { - val resources = context.packageManager.getResourcesForApplication(packageName) - val resId = resources.getIdentifier("grayscale_icon_map", "xml", packageName) - iconDao.deleteGrayscaleIcons(packageName) - if (resId == 0) { - return@runInTransaction - } - val icons = mutableListOf() - val parser = resources.getXml(resId) - loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.eventType != XmlPullParser.START_TAG) continue - when (parser.name) { - "icon" -> { - val drawable = - parser.getAttributeResourceValue(null, "drawable", 0).toString() - val pkg = parser.getAttributeValue(null, "package") - val componentName = ComponentName(pkg, pkg) - val icon = IconPackIcon( - drawable = drawable, - componentName = componentName, - iconPack = packageName, - type = "greyscale_icon" - ) - icons.add(icon) - } - } - if (icons.size >= 100) { - iconDao.insertAll(icons.map { it.toDatabaseEntity() }) - icons.clear() - } - } - if (icons.isNotEmpty()) { - iconDao.insertAll(icons.map { it.toDatabaseEntity() }) - } - } catch (e: PackageManager.NameNotFoundException) { - iconDao.deleteGrayscaleIcons(packageName) - return@runInTransaction - } - - } - } -} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt index b713b623..e398d95c 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt @@ -1,233 +1,53 @@ package de.mm20.launcher2.icons.loaders -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.XmlResourceParser -import android.util.Log -import de.mm20.launcher2.crashreporter.CrashReporter +import androidx.room.withTransaction import de.mm20.launcher2.database.AppDatabase +import de.mm20.launcher2.icons.BuildConfig import de.mm20.launcher2.icons.IconPack -import de.mm20.launcher2.icons.IconPackIcon -import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException -import org.xmlpull.v1.XmlPullParserFactory -import java.io.IOException -import java.io.Reader +import de.mm20.launcher2.icons.IconPackComponent -class IconPackInstaller( - private val context: Context, +abstract class IconPackInstaller( private val database: AppDatabase, ) { - - fun installIcons() { - val packs = loadInstalledPacks(context) - val iconDao = database.iconDao() - - for (pack in packs) { - try { - installIconPack(pack) - } catch (e: PackageManager.NameNotFoundException) { - continue - } - } - - iconDao.uninstallIconPacksExcept( - packs.map { it.packageName }.toList() - ) - } - - private fun loadInstalledPacks(context: Context): List { - val packs = mutableListOf() - val pm = context.packageManager - var intent = Intent("app.lawnchair.icons.THEMED_ICON") - val themedPacks = pm.queryIntentActivities(intent, 0) - packs.addAll(themedPacks.map { IconPack(context, it, true) }) - intent = Intent("org.adw.ActivityStarter.THEMES") - val adwPacks = pm.queryIntentActivities(intent, 0) - packs.addAll(adwPacks.map { IconPack(context, it, false) }) - intent = Intent("com.novalauncher.THEME") - val novaPacks = pm.queryIntentActivities(intent, 0) - packs.addAll(novaPacks.map { IconPack(context, it, false) }) - return packs.distinctBy { it.packageName } - } - - private fun installIconPack(iconPack: IconPack) { - val pkgName = iconPack.packageName - - val icons = mutableListOf() - database.runInTransaction { - try { - val res = context.packageManager.getResourcesForApplication(pkgName) - val parser: XmlPullParser - var inStream: Reader? = null - val xmlId = res.getIdentifier("appfilter", "xml", pkgName) - val rawId = res.getIdentifier("appfilter", "raw", pkgName) - parser = when { - xmlId != 0 -> res.getXml(xmlId) - rawId != 0 -> { - inStream = res.openRawResource(rawId).reader() - XmlPullParserFactory.newInstance().newPullParser().apply { - setInput(inStream) - } - } - - else -> { - val iconPackContext = context.createPackageContext( - pkgName, - Context.CONTEXT_IGNORE_SECURITY - ) - inStream = try { - iconPackContext.assets.open("appfilter.xml").reader() - } catch (e: IOException) { - CrashReporter.logException(e) - Log.e( - "MM20", - "appfilter.xml not found in $pkgName. Searched locations: res/xml/appfilter.xml, res/raw/appfilter.xml, assets/appfilter.xml" - ) - return@runInTransaction - } - XmlPullParserFactory.newInstance().newPullParser().apply { - setInput(inStream) - } - } - } - val iconDao = database.iconDao() - - iconDao.deleteIconPack(iconPack.toDatabaseEntity()) - iconDao.deleteIcons(iconPack.packageName) - - loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.eventType != XmlPullParser.START_TAG) continue - when (parser.name) { - "item" -> { - val component = parser.getAttributeValue(null, "component") - ?: continue@loop - val drawable = parser.getAttributeValue(null, "drawable") - ?: continue@loop - if (component.length <= 14) continue@loop - val componentName = ComponentName.unflattenFromString( - component.substring( - 14, - component.lastIndex - ) - ) - ?: continue@loop - - val name = parser.getAttributeValue(null, "name") - - val icon = IconPackIcon( - componentName = componentName, - drawable = drawable, - iconPack = pkgName, - name = name, - themed = iconPack.themed, - type = "app" - ) - icons.add(icon) - } - - "calendar" -> { - val component = parser.getAttributeValue(null, "component") - ?: continue@loop - val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop - if (component.length < 14) continue@loop - val componentName = ComponentName.unflattenFromString( - component.substring( - 14, - component.lastIndex - ) - ) - ?: continue@loop - - val name = parser.getAttributeValue(null, "name") - - val icon = IconPackIcon( - componentName = componentName, - drawable = drawable, - iconPack = pkgName, - type = "calendar", - themed = iconPack.themed, - name = name, - ) - icons.add(icon) - } - - "iconback" -> { - for (i in 0 until parser.attributeCount) { - if (parser.getAttributeName(i).startsWith("img")) { - val drawable = parser.getAttributeValue(i) - val icon = IconPackIcon( - componentName = null, - drawable = drawable, - iconPack = pkgName, - type = "iconback" - ) - icons.add(icon) - } - } - } - - "iconupon" -> { - for (i in 0 until parser.attributeCount) { - if (parser.getAttributeName(i).startsWith("img")) { - val drawable = parser.getAttributeValue(i) - val icon = IconPackIcon( - componentName = null, - drawable = drawable, - iconPack = pkgName, - type = "iconupon" - ) - icons.add(icon) - } - } - } - - "iconmask" -> { - for (i in 0 until parser.attributeCount) { - if (parser.getAttributeName(i).startsWith("img")) { - val drawable = parser.getAttributeValue(i) - val icon = IconPackIcon( - componentName = null, - drawable = drawable, - iconPack = pkgName, - type = "iconmask" - ) - icons.add(icon) - } - } - } - - "scale" -> { - val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull() - ?: continue@loop - iconPack.scale = scale - } - } + suspend fun install(iconPack: IconPack) { + var pack = iconPack + val dao = database.iconDao() + database.withTransaction { + dao.deleteIconPack(iconPack.toDatabaseEntity()) + dao.deleteIcons(iconPack.packageName) + val icons = mutableListOf() + val installerScope = object: IconPackInstallerScope { + override suspend fun addIcon(icon: IconPackComponent) { + icons.add(icon) if (icons.size >= 100) { - iconDao.insertAll(icons.map { it.toDatabaseEntity() }) + dao.insertAll(icons.map { it.toDatabaseEntity() }) icons.clear() } } - if (icons.isNotEmpty()) { - iconDao.insertAll(icons.map { it.toDatabaseEntity() }) + override suspend fun updatePackInfo(update: (IconPack) -> IconPack) { + pack = update(iconPack) } - iconDao.installIconPack(iconPack.toDatabaseEntity()) - - (parser as? XmlResourceParser)?.close() - inStream?.close() - - Log.d("MM20", "Icon pack has been installed successfully") - } catch (e: PackageManager.NameNotFoundException) { - Log.e("MM20", "Could not install icon pack $pkgName: package not found.") - } catch (e: XmlPullParserException) { - CrashReporter.logException(e) } - + installerScope.buildIconPack(iconPack) + if (icons.isNotEmpty()) dao.insertAll(icons.map { it.toDatabaseEntity() }) + dao.installIconPack(pack.toDatabaseEntity()) } } + abstract suspend fun IconPackInstallerScope.buildIconPack(iconPack: IconPack) + abstract fun getInstalledIconPacks(): List + + suspend fun isInstalledAndUpToDate(iconPack: IconPack): Boolean { + if (BuildConfig.DEBUG) return false + val dao = database.iconDao() + val installed = dao.getIconPack(iconPack.packageName)?.let { IconPack(it) } ?: return false + return installed.version == iconPack.version + } +} + +interface IconPackInstallerScope { + suspend fun addIcon(icon: IconPackComponent) + suspend fun updatePackInfo(update: (IconPack) -> IconPack) } \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomIconPackIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomIconPackIconProvider.kt index 1c82fe14..d1f0d2c6 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomIconPackIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomIconPackIconProvider.kt @@ -13,7 +13,8 @@ class CustomIconPackIconProvider( override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { return iconPackManager.getIcon( customIcon.iconPackPackage, - ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null + customIcon.iconPackageName, + customIcon.iconActivityName, ) } } \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt index da1060e5..d3038107 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt @@ -10,6 +10,6 @@ class CustomThemedIconProvider( private val iconPackManager: IconPackManager, ): IconProvider { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { - return iconPackManager.getThemedIcon(customIcon.iconPackageName) + return null //iconPackManager.getThemedIcon(customIcon.iconPackageName) } } \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt index bd85b485..a37aaf3f 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt @@ -16,8 +16,7 @@ class IconPackIconProvider( override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { if (searchable !is LauncherApp) return null - val component = ComponentName(searchable.`package`, searchable.activity) - return iconPackManager.getIcon(iconPack.packageName, component) + return iconPackManager.getIcon(iconPack.packageName, searchable.`package`, searchable.activity) ?: iconPackManager.generateIcon( context, iconPack.packageName, diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt deleted file mode 100644 index 15758af0..00000000 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt +++ /dev/null @@ -1,15 +0,0 @@ -package de.mm20.launcher2.icons.providers - -import de.mm20.launcher2.icons.* -import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp - -internal class ThemedIconProvider( - private val iconPackManager: IconPackManager, -) : IconProvider { - - override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { - if (searchable !is LauncherApp) return null - return iconPackManager.getThemedIcon(searchable.`package`) - } -} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedPlaceholderIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedPlaceholderIconProvider.kt index c09521e1..14f394da 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedPlaceholderIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedPlaceholderIconProvider.kt @@ -22,6 +22,9 @@ internal class ThemedPlaceholderIconProvider( is ClockLayer -> TintedClockLayer( scale = layer.scale, color = 0, + defaultHour = layer.defaultHour, + defaultMinute = layer.defaultMinute, + defaultSecond = layer.defaultSecond, sublayers = layer.sublayers, ) is ColorLayer -> layer.copy(color = 0) diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt index f4753d2c..aa51e6a5 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt @@ -14,6 +14,9 @@ internal class ForceThemedIconTransformation : LauncherIconTransformation { return when(layer) { is ClockLayer -> TintedClockLayer( scale = layer.scale, + defaultHour = layer.defaultHour, + defaultMinute = layer.defaultMinute, + defaultSecond = layer.defaultSecond, sublayers = layer.sublayers, ) is ColorLayer -> layer.copy(color = 0) diff --git a/settings.gradle.kts b/settings.gradle.kts index fc599dfa..9bc765be 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -138,7 +138,7 @@ dependencyResolutionManagement { library("androidx.datastore", "androidx.datastore", "datastore") .version("1.0.0") - version("androidx.room", "2.5.0-alpha03") + version("androidx.room", "2.5.0") library("androidx.roomruntime", "androidx.room", "room-runtime") .versionRef("androidx.room") library("androidx.roomcompiler", "androidx.room", "room-compiler")