Restructure / cleanup / improve icon pack handling

This commit is contained in:
MM20 2023-02-27 22:36:46 +01:00
parent 96f69356c4
commit 7168169e35
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
25 changed files with 1441 additions and 725 deletions

View File

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

View File

@ -15,6 +15,9 @@ data class ColorLayer(
data class ClockLayer(
val sublayers: List<ClockSublayer>,
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<ClockSublayer>,
val defaultHour: Int = 0,
val defaultMinute: Int = 0,
val defaultSecond: Int = 0,
val scale: Float,
val color: Int = 0,
) : LauncherIconLayer

View File

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

View File

@ -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')"
]
}
}

View File

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

View File

@ -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<IconEntity>)
suspend fun insertAll(icons: List<IconEntity>)
@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<IconEntity>
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
@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<IconEntity>
@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<IconEntity>
@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<IconEntity>) {
deleteIconPack(iconPack)
deleteIcons(iconPack.packageName)
insertAll(icons)
installIconPack(iconPack)
}
@Transaction
suspend fun installGrayscaleIconMap(packageName: String, icons: List<IconEntity>) {
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<List<IconPackEntity>>
@Delete
fun deleteIconPack(iconPack: IconPackEntity)
@Query("SELECT * FROM IconPack WHERE packageName = :packageName AND version = :version")
suspend fun getPacks(packageName: String, version: String): List<IconPackEntity>
@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<String>)
@Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs) AND type = 'greyscale_icon'")
fun deleteAllGrayscaleIconsExcept(packs: List<String>)
@Query("DELETE FROM IconPack WHERE packageName NOT IN (:packs)")
fun deleteAllPacksExcept(packs: List<String>)
@Transaction
fun uninstallIconPacksExcept(packs: List<String>) {
deleteAllIconsPackIconsExcept(packs)
deleteAllPacksExcept(packs)
}
@Query("SELECT drawable FROM Icons WHERE iconPack = :pack AND type = 'iconback'")
suspend fun getIconBacks(pack: String): List<String>
@ -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?
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<IconPackIcon> {
suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackAppIcon> {
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<IconPackIcon> {
suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List<IconPackAppIcon> {
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<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.searchGreyscaleIcons("%$query%").map {
IconPackIcon(it)
}
}
}

View File

@ -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<List<IconProvider>> = 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<Unit>(1)
private var transformations: MutableStateFlow<List<LauncherIconTransformation>> =
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<IconProvider>()
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<IconProvider>()
val transformations = mutableListOf<LauncherIconTransformation>()
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<LauncherIconTransformation>()
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<CustomIconWithPreview> {
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?) {

View File

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

View File

@ -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<IconPack> {
val packs = mutableListOf<IconPack>()
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 }
}
}

View File

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

View File

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

View File

@ -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<IconPack> {
val packs = mutableListOf<IconPack>()
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<IconPackIcon>()
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<IconPackComponent>()
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<IconPack>
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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