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) { when (layer) {
is ClockLayer -> { 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 -> { is TintedClockLayer -> {
ClockLayer( ClockLayer(
layer.sublayers, layer.sublayers,
scale = layer.scale, scale = layer.scale,
defaultSecond = layer.defaultSecond,
defaultMinute = layer.defaultMinute,
defaultHour = layer.defaultHour,
tintColor = if (layer.color == 0) defaultTintColor tintColor = if (layer.color == 0) defaultTintColor
else Color(getTone(layer.color, colorTone)) else Color(getTone(layer.color, colorTone))
) )
@ -323,6 +333,9 @@ private fun getTone(argb: Int, tone: Int): Int {
@Composable @Composable
private fun ClockLayer( private fun ClockLayer(
sublayers: List<ClockSublayer>, sublayers: List<ClockSublayer>,
defaultMinute: Int,
defaultHour: Int,
defaultSecond: Int,
scale: Float, scale: Float,
tintColor: Color?, tintColor: Color?,
) { ) {
@ -331,21 +344,22 @@ private fun ClockLayer(
} }
val second = remember { val second = remember {
Animatable(time.second.toFloat()) Animatable((time.second - defaultSecond).toFloat())
} }
val minute = remember { val minute = remember {
Animatable(time.minute.toFloat() + time.second.toFloat() / 60f) Animatable((time.minute - defaultMinute).toFloat() + (time.second - defaultSecond).toFloat() / 60f)
} }
val hour = remember { val hour = remember {
Animatable(time.hour.toFloat() + time.minute.toFloat() / 60f) Animatable((time.hour - defaultHour).toFloat() + (time.minute + defaultMinute).toFloat() / 60f)
} }
LaunchedEffect(time) { LaunchedEffect(time) {
val h = time.hour.toFloat() + time.minute.toFloat() / 60f val h = (time.hour - defaultHour).toFloat() + (time.minute - defaultSecond).toFloat() / 60f
val m = time.minute.toFloat() + time.second.toFloat() / 60f val m =
val s = time.second.toFloat() + (time.nano / 1000000f) / 1000f (time.minute - defaultMinute).toFloat() + (time.second - defaultSecond).toFloat() / 60f
val s = (time.second - defaultSecond).toFloat() + (time.nano / 1000000f) / 1000f
second.snapTo(s) second.snapTo(s)
hour.snapTo(h) hour.snapTo(h)
minute.snapTo(m) minute.snapTo(m)

View File

@ -15,6 +15,9 @@ data class ColorLayer(
data class ClockLayer( data class ClockLayer(
val sublayers: List<ClockSublayer>, val sublayers: List<ClockSublayer>,
val defaultHour: Int = 0,
val defaultMinute: Int = 0,
val defaultSecond: Int = 0,
val scale: Float, val scale: Float,
) : LauncherIconLayer ) : LauncherIconLayer
@ -38,6 +41,9 @@ data class TintedIconLayer(
data class TintedClockLayer( data class TintedClockLayer(
val sublayers: List<ClockSublayer>, val sublayers: List<ClockSublayer>,
val defaultHour: Int = 0,
val defaultMinute: Int = 0,
val defaultSecond: Int = 0,
val scale: Float, val scale: Float,
val color: Int = 0, val color: Int = 0,
) : LauncherIconLayer ) : LauncherIconLayer

View File

@ -48,7 +48,7 @@ dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
api(libs.androidx.roomruntime) api(libs.androidx.roomruntime)
kapt(libs.androidx.roomcompiler) kapt(libs.androidx.roomcompiler)
implementation(libs.androidx.room) api(libs.androidx.room)
implementation(libs.koin.android) implementation(libs.koin.android)
implementation(project(":core:i18n")) 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.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import de.mm20.launcher2.database.entities.* import de.mm20.launcher2.database.entities.*
import de.mm20.launcher2.database.migrations.Migration_10_11 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_17_18
import de.mm20.launcher2.database.migrations.Migration_18_19 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_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_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8 import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9 import de.mm20.launcher2.database.migrations.Migration_8_9
@ -34,7 +36,7 @@ import de.mm20.launcher2.database.migrations.Migration_9_10
WidgetEntity::class, WidgetEntity::class,
CustomAttributeEntity::class, CustomAttributeEntity::class,
SearchActionEntity::class, SearchActionEntity::class,
], version = 20, exportSchema = true ], version = 21, exportSchema = true
) )
@TypeConverters(ComponentNameConverter::class, StringListConverter::class) @TypeConverters(ComponentNameConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -101,6 +103,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_17_18(), Migration_17_18(),
Migration_18_19(), Migration_18_19(),
Migration_19_20(), Migration_19_20(),
Migration_20_21(),
).build() ).build()
if (_instance == null) _instance = instance if (_instance == null) _instance = instance
return 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.IconEntity
import de.mm20.launcher2.database.entities.IconPackEntity import de.mm20.launcher2.database.entities.IconPackEntity
internal val AppTypes = listOf("app", "calendar", "clock")
@Dao @Dao
interface IconDao { interface IconDao {
@Insert @Insert
fun insertAll(icons: List<IconEntity>) suspend fun insertAll(icons: List<IconEntity>)
@Query("SELECT drawable FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack") @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 getIconName(componentName: String, iconPack: String): String? 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") @Query("SELECT * FROM Icons WHERE packageName = :packageName AND (activityName = :activityName OR activityName IS NULL) AND type IN ('app', 'calendar', 'clock')")
suspend fun getIcon(componentName: String, iconPack: String): IconEntity? suspend fun getIconsFromAllPacks(packageName: String, activityName: String): List<IconEntity>
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')") @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 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")
suspend fun searchIconPackIcons( suspend fun searchIconPackIcons(
componentQuery: String, componentQuery: String,
nameQuery: String, nameQuery: String,
@ -28,30 +27,10 @@ interface IconDao {
limit: Int = 100 limit: Int = 100
): List<IconEntity> ): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'greyscale_icon') AND componentName LIKE :query GROUP BY componentName ORDER BY drawable LIMIT :limit") @Query("DELETE FROM Icons WHERE iconPack = :iconPack")
suspend fun searchGreyscaleIcons(query: String, limit: Int = 100): List<IconEntity>
@Query("DELETE FROM Icons WHERE iconPack = :iconPack AND type != 'greyscale_icon'")
fun deleteIcons(iconPack: String) fun deleteIcons(iconPack: String)
@Query("DELETE FROM Icons WHERE iconPack = :iconPack AND type = 'greyscale_icon'") @Insert(onConflict = OnConflictStrategy.REPLACE)
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
fun installIconPack(iconPack: IconPackEntity) fun installIconPack(iconPack: IconPackEntity)
@Query("SELECT * FROM IconPack") @Query("SELECT * FROM IconPack")
@ -60,35 +39,9 @@ interface IconDao {
@Query("SELECT * FROM IconPack WHERE packageName = :packageName LIMIT 1") @Query("SELECT * FROM IconPack WHERE packageName = :packageName LIMIT 1")
suspend fun getIconPack(packageName: String): IconPackEntity? suspend fun getIconPack(packageName: String): IconPackEntity?
@Query("SELECT * FROM IconPack")
fun getInstalledIconPacksLiveData(): LiveData<List<IconPackEntity>>
@Delete @Delete
fun deleteIconPack(iconPack: IconPackEntity) 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'") @Query("SELECT drawable FROM Icons WHERE iconPack = :pack AND type = 'iconback'")
suspend fun getIconBacks(pack: String): List<String> suspend fun getIconBacks(pack: String): List<String>
@ -100,7 +53,4 @@ interface IconDao {
@Query("SELECT scale FROM IconPack WHERE packageName = :pack") @Query("SELECT scale FROM IconPack WHERE packageName = :pack")
suspend fun getScale(pack: String): Float? 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 package de.mm20.launcher2.database.entities
import android.content.ComponentName
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "Icons") @Entity(tableName = "Icons")
data class IconEntity( data class IconEntity(
val type: String, val type: String,
val componentName: ComponentName?, val packageName: String? = null,
val activityName: String? = null,
val drawable: String?, val drawable: String?,
val extras: String? = null,
val iconPack: String, val iconPack: String,
val name: String?, val name: String? = null,
val themed: Boolean = false, val themed: Boolean = false,
@PrimaryKey(autoGenerate = true) val id : Long? = null @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 package de.mm20.launcher2.data.customattrs
import android.content.ComponentName
import android.util.Log import android.util.Log
import de.mm20.launcher2.database.entities.CustomAttributeEntity import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
@ -76,10 +77,20 @@ sealed class CustomIcon : CustomAttribute {
val type = payload.getString("type") val type = payload.getString("type")
return when (type) { return when (type) {
"custom_icon_pack_icon" -> { "custom_icon_pack_icon" -> {
val legacyComponentName = payload.optString("icon").let { ComponentName.unflattenFromString(it) }
if (legacyComponentName != null) {
CustomIconPackIcon( CustomIconPackIcon(
iconComponentName = payload.getString("icon"), iconPackageName = legacyComponentName.packageName,
iconActivityName = legacyComponentName.className,
iconPackPackage = payload.getString("icon_pack") 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" -> { "custom_themed_icon" -> {
CustomThemedIcon( CustomThemedIcon(
@ -105,12 +116,14 @@ sealed class CustomIcon : CustomAttribute {
data class CustomIconPackIcon( data class CustomIconPackIcon(
val iconPackPackage: String, val iconPackPackage: String,
val iconComponentName: String, val iconPackageName: String,
val iconActivityName: String?,
) : CustomIcon() { ) : CustomIcon() {
override fun toDatabaseValue(): String { override fun toDatabaseValue(): String {
return jsonObjectOf( return jsonObjectOf(
"type" to "custom_icon_pack_icon", "type" to "custom_icon_pack_icon",
"icon" to iconComponentName, "package" to iconPackageName,
"activity" to iconActivityName,
"icon_pack" to iconPackPackage, "icon_pack" to iconPackPackage,
).toString() ).toString()
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.icons package de.mm20.launcher2.icons
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import de.mm20.launcher2.database.entities.IconPackEntity import de.mm20.launcher2.database.entities.IconPackEntity
@ -8,13 +9,13 @@ data class IconPack(
val name: String, val name: String,
val packageName: String, val packageName: String,
val version: String, val version: String,
var scale: Float = 1f, val scale: Float = 1f,
val themed: Boolean = false, val themed: Boolean = false,
) { ) {
constructor(entity: IconPackEntity) : this( constructor(entity: IconPackEntity) : this(
name = entity.name, name = entity.name,
packageName = entity.packageName, packageName = entity.packageName,
version = entity.packageName, version = entity.version,
scale = entity.scale, scale = entity.scale,
themed = entity.themed, themed = entity.themed,
) )
@ -30,6 +31,17 @@ data class IconPack(
themed = themed, 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 { fun toDatabaseEntity(): IconPackEntity {
return IconPackEntity( return IconPackEntity(
name = name, name = name,

View File

@ -1,33 +1,188 @@
package de.mm20.launcher2.icons package de.mm20.launcher2.icons
import android.content.ComponentName
import de.mm20.launcher2.database.entities.IconEntity 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( sealed interface IconPackComponent {
val type: String, val iconPack: String
val componentName: ComponentName?, fun toDatabaseEntity(): IconEntity
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,
)
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( return IconEntity(
type = type, type = "iconback",
componentName = componentName,
drawable = drawable, drawable = drawable,
iconPack = iconPack, 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, name = name,
themed = themed, 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 android.util.Log
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.loaders.GrayscaleMapInstaller import de.mm20.launcher2.icons.loaders.AppFilterIconPackInstaller
import de.mm20.launcher2.icons.loaders.IconPackInstaller import de.mm20.launcher2.icons.loaders.GrayscaleMapIconPackInstaller
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.ktx.randomElementOrNull import de.mm20.launcher2.ktx.randomElementOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -50,16 +50,33 @@ class IconPackManager(
} }
} }
suspend fun updateIconPacks() { private var updateIconPacksMutex = Mutex()
withContext(Dispatchers.IO) { suspend fun updateIconPacks(): Boolean {
IconPackInstaller(context, appDatabase).installIcons() var iconsHaveBeenUpdated = false
GrayscaleMapInstaller(context, appDatabase).installIcons() 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( suspend fun getIcon(
iconPack: String, iconPack: String,
componentName: ComponentName, packageName: String,
activityName: String?,
): LauncherIcon? { ): LauncherIcon? {
val res = try { val res = try {
context.packageManager.getResourcesForApplication(iconPack) context.packageManager.getResourcesForApplication(iconPack)
@ -68,70 +85,18 @@ class IconPackManager(
return null return null
} }
val iconDao = appDatabase.iconDao() val iconDao = appDatabase.iconDao()
val icon = iconDao.getIcon(componentName.flattenToString(), iconPack) val icon = iconDao.getIcon(packageName, activityName, iconPack)?.let { IconPackAppIcon(it) }
?: return null ?: return null
val drawableName = icon.drawable ?: return null if (icon is CalendarIcon) {
return getIconPackCalendarIcon(icon, res)
if (icon.type == "calendar") { } else if (icon is AppIcon) {
return getIconPackCalendarIcon(context, iconPack, drawableName, icon.themed) return getIconPackStaticIcon(icon, res)
} else if (icon is ClockIcon) {
return getIconPackClockIcon(icon, res)
} }
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 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
)
}
}
}
suspend fun generateIcon( suspend fun generateIcon(
context: Context, context: Context,
@ -233,10 +198,10 @@ class IconPackManager(
) )
} }
suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackIcon> { suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackAppIcon> {
val iconDao = appDatabase.iconDao() val iconDao = appDatabase.iconDao()
return iconDao.getIconsFromAllPacks(componentName.flattenToString()) return iconDao.getIconsFromAllPacks(componentName.packageName, componentName.shortClassName)
.map { IconPackIcon(it) } .mapNotNull { IconPackAppIcon(it) }
} }
private suspend fun getIconBack(iconPack: String): String? { private suspend fun getIconBack(iconPack: String): String? {
@ -262,25 +227,90 @@ class IconPackManager(
return iconDao.getScale(iconPack) ?: 1f return iconDao.getScale(iconPack) ?: 1f
} }
private fun getIconPackCalendarIcon( private fun getIconPackStaticIcon(
context: Context, icon: AppIcon,
iconPack: String, resources: Resources,
baseIconName: String,
themed: Boolean,
): LauncherIcon? { ): LauncherIcon? {
val resources = try { val resId =
context.packageManager.getResourcesForApplication(iconPack) resources.getIdentifier(icon.drawable, "drawable", icon.iconPack).takeIf { it != 0 }
} catch (e: PackageManager.NameNotFoundException) { ?: return null
val drawable = try {
ResourcesCompat.getDrawable(resources, resId, context.theme) ?: return null
} catch (e: Resources.NotFoundException) {
return null return null
} }
val drawableIds = (1..31).map { return when {
val drawableName = baseIconName + it icon.themed && drawable is AdaptiveIconDrawable -> {
val id = resources.getIdentifier(drawableName, "drawable", iconPack) 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 if (id == 0) return null
id id
}.toIntArray() }.toIntArray()
if (themed) {
if (icon.themed) {
return ThemedDynamicCalendarIcon( return ThemedDynamicCalendarIcon(
resources = resources, resources = resources,
resourceIds = drawableIds, resourceIds = drawableIds,
@ -292,142 +322,91 @@ class IconPackManager(
) )
} }
suspend fun getThemedIcon(packageName: String): LauncherIcon? { private fun getIconPackClockIcon(
val icon = getGreyscaleIcon(packageName) ?: return null icon: ClockIcon,
val resId = icon.drawable?.toIntOrNull() ?: return null resources: Resources,
try { ): LauncherIcon? {
val resources = context.packageManager.getResourcesForApplication(icon.iconPack) var drawable = try {
return getThemedClockIcon(resources, resId) ?: getThemedCalendarIcon( resources.getIdentifier(icon.drawable, "drawable", icon.iconPack).takeIf { it != 0 }
resources, ?.let { ResourcesCompat.getDrawable(resources, it, null) }
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()
)
} catch (e: Resources.NotFoundException) { } catch (e: Resources.NotFoundException) {
return null null
} } ?: return null
}
private fun getThemedClockIcon(resources: Resources, resId: Int): LauncherIcon? { val background = (drawable as? AdaptiveIconDrawable)?.background
try { val foreground = (drawable as? AdaptiveIconDrawable)?.foreground ?: drawable
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
}
"com.android.launcher3.HOUR_LAYER_INDEX" -> { if (foreground !is LayerDrawable) return null
i++
hourIndex = array.getInt(i, -1).takeIf { it != -1 }
}
"com.android.launcher3.MINUTE_LAYER_INDEX" -> { val layers = (0 until foreground.numberOfLayers).map {
i++ val drw = foreground.getDrawable(it)
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 && minuteIndex != null && hourIndex != null) {
return 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( ClockSublayer(
drawable = drw, drawable = drw,
role = when (it) { role = when (it) {
hourIndex -> ClockSublayerRole.Hour icon.config.hourLayer -> ClockSublayerRole.Hour
minuteIndex -> ClockSublayerRole.Minute icon.config.minuteLayer -> ClockSublayerRole.Minute
icon.config.secondLayer -> ClockSublayerRole.Second
else -> ClockSublayerRole.Static else -> ClockSublayerRole.Static
} }
) )
}, }
return when {
icon.themed && drawable is AdaptiveIconDrawable -> {
StaticLauncherIcon(
foregroundLayer = TintedClockLayer(
defaultHour = icon.config.defaultHour,
defaultMinute = icon.config.defaultMinute,
defaultSecond = icon.config.defaultSecond,
sublayers = layers,
scale = 1.5f, scale = 1.5f,
), ),
backgroundLayer = ColorLayer() backgroundLayer = ColorLayer(),
) )
} }
} catch (e: Resources.NotFoundException) { icon.themed -> {
} StaticLauncherIcon(
return null foregroundLayer = TintedClockLayer(
} defaultHour = icon.config.defaultHour,
defaultMinute = icon.config.defaultMinute,
private fun getThemedCalendarIcon( defaultSecond = icon.config.defaultSecond,
resources: Resources, sublayers = layers,
resId: Int, scale = 1f,
iconProviderPackage: String ),
): LauncherIcon? { backgroundLayer = ColorLayer(),
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 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,
)
}
}
} }
suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List<IconPackIcon> { suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List<IconPackAppIcon> {
val iconDao = appDatabase.iconDao() val iconDao = appDatabase.iconDao()
val drawableQuery = query.replace(" ", "_").lowercase() val drawableQuery = query.replace(" ", "_").lowercase()
return iconDao.searchIconPackIcons( return iconDao.searchIconPackIcons(
@ -435,17 +414,11 @@ class IconPackManager(
componentQuery = "%$query%", componentQuery = "%$query%",
nameQuery = "%$query%", nameQuery = "%$query%",
iconPack = iconPack?.packageName, iconPack = iconPack?.packageName,
).map { ).mapNotNull {
IconPackIcon(it) 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 package de.mm20.launcher2.icons
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter 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.IconProvider
import de.mm20.launcher2.icons.providers.PlaceholderIconProvider import de.mm20.launcher2.icons.providers.PlaceholderIconProvider
import de.mm20.launcher2.icons.providers.SystemIconProvider 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.ThemedPlaceholderIconProvider
import de.mm20.launcher2.icons.providers.getFirstIcon import de.mm20.launcher2.icons.providers.getFirstIcon
import de.mm20.launcher2.icons.transformations.ForceThemedIconTransformation import de.mm20.launcher2.icons.transformations.ForceThemedIconTransformation
@ -39,11 +39,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.internal.ChannelFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -67,6 +69,11 @@ class IconRepository(
private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf()) private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf())
private var placeholderProvider: IconProvider? = null 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>> = private var transformations: MutableStateFlow<List<LauncherIconTransformation>> =
MutableStateFlow( MutableStateFlow(
listOf() listOf()
@ -83,8 +90,11 @@ class IconRepository(
addDataScheme("package") addDataScheme("package")
}) })
iconPacksUpdated.tryEmit(Unit)
scope.launch { scope.launch {
dataStore.data.map { it.icons }.distinctUntilChanged().collectLatest { settings -> dataStore.data.map { it.icons }.distinctUntilChanged().collectLatest { settings ->
iconPacksUpdated.collectLatest {
val placeholderProvider = if (settings.themedIcons) { val placeholderProvider = if (settings.themedIcons) {
ThemedPlaceholderIconProvider(context) ThemedPlaceholderIconProvider(context)
} else { } else {
@ -106,9 +116,6 @@ class IconRepository(
Log.w("MM20", "Icon pack ${settings.iconPack} not found") Log.w("MM20", "Icon pack ${settings.iconPack} not found")
} }
} }
if (settings.themedIcons) {
providers.add(ThemedIconProvider(iconPackManager))
}
providers.add(DynamicClockIconProvider(context, settings.themedIcons)) providers.add(DynamicClockIconProvider(context, settings.themedIcons))
providers.add(CalendarIconProvider(context, settings.themedIcons)) providers.add(CalendarIconProvider(context, settings.themedIcons))
if (!isAtLeastApiLevel(33)) { if (!isAtLeastApiLevel(33)) {
@ -131,6 +138,7 @@ class IconRepository(
} }
} }
} }
}
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon> = channelFlow { fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon> = channelFlow {
@ -215,7 +223,9 @@ class IconRepository(
fun requestIconPackListUpdate() { fun requestIconPackListUpdate() {
scope.launch { scope.launch {
iconPackManager.updateIconPacks() iconPackManager.updateIconPacks().also {
if (it)iconPacksUpdated.tryEmit(Unit)
}
} }
} }
@ -285,24 +295,14 @@ class IconRepository(
iconPackIcons.mapNotNull { iconPackIcons.mapNotNull {
CustomIconPackIcon( CustomIconPackIcon(
iconPackPackage = it.iconPack, iconPackPackage = it.iconPack,
iconComponentName = it.componentName?.flattenToString() iconActivityName = it.activityName,
?: return@mapNotNull null 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( transformationOptions.add(
ForceThemedIcon ForceThemedIcon
) )
}
} else { } else {
transformationOptions.add( transformationOptions.add(
ForceThemedIcon ForceThemedIcon
@ -359,31 +359,18 @@ class IconRepository(
suspend fun searchCustomIcons(query: String, iconPack: IconPack?): List<CustomIconWithPreview> { suspend fun searchCustomIcons(query: String, iconPack: IconPack?): List<CustomIconWithPreview> {
val transformations = this.transformations.first() val transformations = this.transformations.first()
val iconPackIcons = iconPackManager.searchIconPackIcon(query, iconPack).mapNotNull { val iconPackIcons = iconPackManager.searchIconPackIcon(query, iconPack).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview( CustomIconWithPreview(
customIcon = CustomIconPackIcon( customIcon = CustomIconPackIcon(
iconPackPackage = it.iconPack, 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 ?.transform(transformations) ?: return@mapNotNull null
) )
} }
val themedIcons = iconPackManager.searchThemedIcons(query).mapNotNull { return iconPackIcons
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
} }
fun setCustomIcon(searchable: SavableSearchable, icon: CustomIcon?) { fun setCustomIcon(searchable: SavableSearchable, icon: CustomIcon?) {

View File

@ -145,23 +145,6 @@ fun AdaptiveIconDrawableCompat.toLauncherIcon(
if (clock != null && clockForeground != null) { if (clock != null && clockForeground != null) {
val clockLayers = (0 until clockForeground.numberOfLayers).map { val clockLayers = (0 until clockForeground.numberOfLayers).map {
val drw = clockForeground.getDrawable(it) 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( ClockSublayer(
drawable = drw, drawable = drw,
role = when (it) { role = when (it) {
@ -175,6 +158,9 @@ fun AdaptiveIconDrawableCompat.toLauncherIcon(
if (themed) { if (themed) {
return StaticLauncherIcon( return StaticLauncherIcon(
foregroundLayer = TintedClockLayer( foregroundLayer = TintedClockLayer(
defaultHour = clock.defaultHour,
defaultMinute = clock.defaultMinute,
defaultSecond = clock.defaultSecond,
sublayers = clockLayers, sublayers = clockLayers,
scale = 1.5f, scale = 1.5f,
), ),
@ -183,6 +169,9 @@ fun AdaptiveIconDrawableCompat.toLauncherIcon(
} }
return StaticLauncherIcon( return StaticLauncherIcon(
foregroundLayer = ClockLayer( foregroundLayer = ClockLayer(
defaultHour = clock.defaultHour,
defaultMinute = clock.defaultMinute,
defaultSecond = clock.defaultSecond,
sublayers = clockLayers, sublayers = clockLayers,
scale = 1.5f, 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 package de.mm20.launcher2.icons.loaders
import android.content.ComponentName import androidx.room.withTransaction
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.database.AppDatabase
import de.mm20.launcher2.icons.BuildConfig
import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.IconPackIcon import de.mm20.launcher2.icons.IconPackComponent
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.Reader
class IconPackInstaller( abstract class IconPackInstaller(
private val context: Context,
private val database: AppDatabase, private val database: AppDatabase,
) { ) {
suspend fun install(iconPack: IconPack) {
fun installIcons() { var pack = iconPack
val packs = loadInstalledPacks(context) val dao = database.iconDao()
val iconDao = database.iconDao() database.withTransaction {
dao.deleteIconPack(iconPack.toDatabaseEntity())
for (pack in packs) { dao.deleteIcons(iconPack.packageName)
try { val icons = mutableListOf<IconPackComponent>()
installIconPack(pack) val installerScope = object: IconPackInstallerScope {
} catch (e: PackageManager.NameNotFoundException) { override suspend fun addIcon(icon: IconPackComponent) {
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) 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
}
}
if (icons.size >= 100) { if (icons.size >= 100) {
iconDao.insertAll(icons.map { it.toDatabaseEntity() }) dao.insertAll(icons.map { it.toDatabaseEntity() })
icons.clear() icons.clear()
} }
} }
if (icons.isNotEmpty()) { override suspend fun updatePackInfo(update: (IconPack) -> IconPack) {
iconDao.insertAll(icons.map { it.toDatabaseEntity() }) 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? { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
return iconPackManager.getIcon( return iconPackManager.getIcon(
customIcon.iconPackPackage, customIcon.iconPackPackage,
ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null customIcon.iconPackageName,
customIcon.iconActivityName,
) )
} }
} }

View File

@ -10,6 +10,6 @@ class CustomThemedIconProvider(
private val iconPackManager: IconPackManager, private val iconPackManager: IconPackManager,
): IconProvider { ): IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { 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? { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null if (searchable !is LauncherApp) return null
val component = ComponentName(searchable.`package`, searchable.activity) return iconPackManager.getIcon(iconPack.packageName, searchable.`package`, searchable.activity)
return iconPackManager.getIcon(iconPack.packageName, component)
?: iconPackManager.generateIcon( ?: iconPackManager.generateIcon(
context, context,
iconPack.packageName, 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( is ClockLayer -> TintedClockLayer(
scale = layer.scale, scale = layer.scale,
color = 0, color = 0,
defaultHour = layer.defaultHour,
defaultMinute = layer.defaultMinute,
defaultSecond = layer.defaultSecond,
sublayers = layer.sublayers, sublayers = layer.sublayers,
) )
is ColorLayer -> layer.copy(color = 0) is ColorLayer -> layer.copy(color = 0)

View File

@ -14,6 +14,9 @@ internal class ForceThemedIconTransformation : LauncherIconTransformation {
return when(layer) { return when(layer) {
is ClockLayer -> TintedClockLayer( is ClockLayer -> TintedClockLayer(
scale = layer.scale, scale = layer.scale,
defaultHour = layer.defaultHour,
defaultMinute = layer.defaultMinute,
defaultSecond = layer.defaultSecond,
sublayers = layer.sublayers, sublayers = layer.sublayers,
) )
is ColorLayer -> layer.copy(color = 0) is ColorLayer -> layer.copy(color = 0)

View File

@ -138,7 +138,7 @@ dependencyResolutionManagement {
library("androidx.datastore", "androidx.datastore", "datastore") library("androidx.datastore", "androidx.datastore", "datastore")
.version("1.0.0") .version("1.0.0")
version("androidx.room", "2.5.0-alpha03") version("androidx.room", "2.5.0")
library("androidx.roomruntime", "androidx.room", "room-runtime") library("androidx.roomruntime", "androidx.room", "room-runtime")
.versionRef("androidx.room") .versionRef("androidx.room")
library("androidx.roomcompiler", "androidx.room", "room-compiler") library("androidx.roomcompiler", "androidx.room", "room-compiler")