Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
+1
View File
@@ -0,0 +1 @@
/build
+57
View File
@@ -0,0 +1,57 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.icons"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.palette)
implementation(libs.materialcomponents.core)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":core:database"))
implementation(project(":core:preferences"))
implementation(project(":core:ktx"))
implementation(project(":core:base"))
implementation(project(":data:applications"))
implementation(project(":core:crashreporter"))
api(project(":data:customattrs"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="org.adw.ActivityStarter.THEMES" />
</intent>
<intent>
<action android:name="com.novalauncher.THEME" />
</intent>
</queries>
</manifest>
@@ -0,0 +1,76 @@
package de.mm20.launcher2.icons
import android.content.res.Resources
import android.graphics.drawable.AdaptiveIconDrawable
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.time.Instant
import java.time.ZoneId
internal class DynamicCalendarIcon(
val resources: Resources,
val resourceIds: IntArray,
val isThemed: Boolean = false,
private var transformations: List<LauncherIconTransformation> = emptyList(),
) : DynamicLauncherIcon, TransformableDynamicLauncherIcon {
init {
if (resourceIds.size < 31) throw IllegalArgumentException("DynamicCalendarIcon resourceIds must at least have 31 items")
}
override suspend fun getIcon(time: Long): StaticLauncherIcon = withContext(Dispatchers.IO) {
val day = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).dayOfMonth
val resId = resourceIds[day - 1]
val drawable = try {
ResourcesCompat.getDrawable(resources, resId, null)
} catch (e: Resources.NotFoundException) {
null
} ?: return@withContext StaticLauncherIcon(
foregroundLayer = TextLayer(day.toString()),
backgroundLayer = ColorLayer()
)
var icon = if (isThemed) {
StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = drawable,
scale = 0.5f,
),
backgroundLayer = ColorLayer()
)
} else if (drawable is AdaptiveIconDrawable) {
return@withContext 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
)
for (transformation in transformations) {
icon = transformation.transform(icon)
}
return@withContext icon
}
override fun setTransformations(transformations: List<LauncherIconTransformation>) {
this.transformations = transformations
}
}
@@ -0,0 +1,26 @@
package de.mm20.launcher2.icons
import de.mm20.launcher2.database.entities.IconPackEntity
data class IconPack(
val name: String,
val packageName: String,
val version: String,
var scale: Float = 1f
) {
constructor(entity: IconPackEntity) : this(
name = entity.name,
packageName = entity.packageName,
version = entity.packageName,
scale = entity.scale
)
fun toDatabaseEntity(): IconPackEntity {
return IconPackEntity(
name = name,
scale = scale,
version = version,
packageName = packageName
)
}
}
@@ -0,0 +1,30 @@
package de.mm20.launcher2.icons
import android.content.ComponentName
import de.mm20.launcher2.database.entities.IconEntity
data class IconPackIcon(
val type: String,
val componentName: ComponentName?,
val drawable: String?,
val iconPack: String,
val scale: Float? = null
) {
constructor(entity: IconEntity) : this(
type = entity.type,
componentName = entity.componentName,
drawable = entity.drawable,
iconPack = entity.iconPack,
scale = entity.scale
)
fun toDatabaseEntity(): IconEntity {
return IconEntity(
type = type,
componentName = componentName,
drawable = drawable,
iconPack = iconPack,
scale = scale
)
}
}
@@ -0,0 +1,659 @@
package de.mm20.launcher2.icons
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.*
import android.graphics.drawable.*
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.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.ktx.randomElementOrNull
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.InputStreamReader
import kotlin.math.roundToInt
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",
)
class IconPackManager(
private val context: Context,
private val appDatabase: AppDatabase,
) {
suspend fun getInstalledIconPacks(): List<IconPack> {
return withContext(Dispatchers.IO) {
appDatabase.iconDao().getInstalledIconPacks().map {
IconPack(it)
}
}
}
suspend fun updateIconPacks() {
withContext(Dispatchers.IO) {
UpdateIconPacksWorker(context).doWork()
}
}
suspend fun getIcon(iconPack: String, componentName: ComponentName): LauncherIcon? {
val res = try {
context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) {
Log.e("MM20", "Icon pack package $iconPack not found!")
return null
}
val iconDao = appDatabase.iconDao()
val icon = iconDao.getIcon(componentName.flattenToString(), iconPack)
?: return null
val drawableName = icon.drawable ?: return null
if (icon.type == "calendar") {
return getIconPackCalendarIcon(context, iconPack, drawableName)
}
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 (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(
context: Context,
iconPack: String,
baseIcon: Drawable,
size: Int
): LauncherIcon? {
val back = getIconBack(iconPack)
val upon = getIconUpon(iconPack)
val mask = getIconMask(iconPack)
val scale = getPackScale(iconPack)
if (back == null && upon == null && mask == null) {
return null
}
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint()
paint.isAntiAlias = true
paint.isFilterBitmap = true
paint.isDither = true
var inBounds: Rect
var outBounds: Rect
val icon = baseIcon.toBitmap(width = size, height = size)
inBounds = Rect(0, 0, icon.width, icon.height)
outBounds = Rect(
(bitmap.width * (1 - scale) * 0.5).roundToInt(),
(bitmap.height * (1 - scale) * 0.5).roundToInt(),
(bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(),
(bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt()
)
canvas.drawBitmap(icon, inBounds, outBounds, paint)
val pack = iconPack
val pm = context.packageManager
val res = try {
pm.getResourcesForApplication(pack)
} catch (e: Resources.NotFoundException) {
return null
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (mask != null) {
res.getIdentifier(mask, "drawable", pack).takeIf { it != 0 }?.let {
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
val maskDrawable = try {
ResourcesCompat.getDrawable(res, it, null) ?: return null
} catch (e: Resources.NotFoundException) {
return null
}
val maskBmp = maskDrawable.toBitmap(size, size)
inBounds = Rect(0, 0, maskBmp.width, maskBmp.height)
outBounds = Rect(0, 0, bitmap.width, bitmap.height)
canvas.drawBitmap(maskBmp, inBounds, outBounds, paint)
}
}
if (upon != null) {
res.getIdentifier(upon, "drawable", pack).takeIf { it != 0 }?.let {
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
val maskDrawable = try {
ResourcesCompat.getDrawable(res, it, null) ?: return null
} catch (e: Resources.NotFoundException) {
return null
}
val maskBmp = maskDrawable.toBitmap(size, size)
inBounds = Rect(0, 0, maskBmp.width, maskBmp.height)
outBounds = Rect(0, 0, bitmap.width, bitmap.height)
canvas.drawBitmap(maskBmp, inBounds, outBounds, paint)
}
}
if (back != null) {
res.getIdentifier(back, "drawable", pack).takeIf { it != 0 }?.let {
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
val maskDrawable = try {
ResourcesCompat.getDrawable(res, it, null) ?: return null
} catch (e: Resources.NotFoundException) {
return null
}
val maskBmp = maskDrawable.toBitmap(size, size)
inBounds = Rect(0, 0, maskBmp.width, maskBmp.height)
outBounds = Rect(0, 0, bitmap.width, bitmap.height)
canvas.drawBitmap(maskBmp, inBounds, outBounds, paint)
}
}
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = BitmapDrawable(context.resources, bitmap),
scale = 1f,
),
backgroundLayer = TransparentLayer
)
}
suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.getIconsFromAllPacks(componentName.flattenToString())
.map { IconPackIcon(it) }
}
private suspend fun getIconBack(iconPack: String): String? {
val iconDao = appDatabase.iconDao()
val iconbacks = iconDao.getIconBacks(iconPack)
return iconbacks.randomElementOrNull()
}
private suspend fun getIconUpon(iconPack: String): String? {
val iconDao = appDatabase.iconDao()
val iconupons = iconDao.getIconUpons(iconPack)
return iconupons.randomElementOrNull()
}
private suspend fun getIconMask(iconPack: String): String? {
val iconDao = appDatabase.iconDao()
val iconmasks = iconDao.getIconMasks(iconPack)
return iconmasks.randomElementOrNull()
}
private suspend fun getPackScale(iconPack: String): Float {
val iconDao = appDatabase.iconDao()
return iconDao.getScale(iconPack) ?: 1f
}
private fun getIconPackCalendarIcon(
context: Context,
iconPack: String,
baseIconName: String
): DynamicCalendarIcon? {
val resources = try {
context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) {
return null
}
val drawableIds = (1..31).map {
val drawableName = baseIconName + it
val id = resources.getIdentifier(drawableName, "drawable", iconPack)
if (id == 0) return null
id
}.toIntArray()
return DynamicCalendarIcon(
resources = resources,
resourceIds = drawableIds
)
}
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()
)
} catch (e: Resources.NotFoundException) {
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
}
"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 && 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(
drawable = drw,
role = when (it) {
hourIndex -> ClockSublayerRole.Hour
minuteIndex -> ClockSublayerRole.Minute
else -> ClockSublayerRole.Static
}
)
},
scale = 1.5f,
),
backgroundLayer = ColorLayer()
)
}
} 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 DynamicCalendarIcon(
resources = resources,
resourceIds = IntArray(31) {
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
},
isThemed = true
)
} catch (e: Resources.NotFoundException) {
}
return null
}
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.searchIconPackIcons("%$query%").map {
IconPackIcon(it)
}
}
suspend fun searchThemedIcons(query: String): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.searchGreyscaleIcons("%$query%").map {
IconPackIcon(it)
}
}
}
class UpdateIconPacksWorker(val context: Context) {
fun doWork() {
val packs = loadInstalledPacks(context).map { it.activityInfo.packageName }
val grayscaleProviders = loadInstalledGreyscaleProviders(context)
val iconDao = AppDatabase.getInstance(context).iconDao()
iconDao.uninstallIconPacksExcept(
packs.union(grayscaleProviders).toList()
)
for (pack in packs) {
try {
val packInfo = context.packageManager.getPackageInfo(pack, 0)
val iconPack = IconPack(
name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(),
packageName = pack,
version = packInfo.versionName
)
//if (iconDao.isInstalled(iconPack)) continue
installIconPack(iconPack)
} catch (e: PackageManager.NameNotFoundException) {
continue
}
}
val supportedGrayscaleMapPackages = SUPPORTED_GRAYSCALE_MAP_PROVIDERS
supportedGrayscaleMapPackages.forEach { installGrayscaleIconMap(it) }
}
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 loadInstalledPacks(context: Context): List<ResolveInfo> {
val packs = mutableListOf<ResolveInfo>()
val pm = context.packageManager
var intent = Intent("org.adw.ActivityStarter.THEMES")
val adwPacks = pm.queryIntentActivities(intent, 0)
packs.addAll(adwPacks)
intent = Intent("com.novalauncher.THEME")
val novaPacks = pm.queryIntentActivities(intent, 0)
novaPacks.forEach {
if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(
it
)
}
packs.sortWith(ResolveInfo.DisplayNameComparator(pm))
return packs
}
private fun installIconPack(iconPack: IconPack) {
val pkgName = iconPack.packageName
val icons = mutableListOf<IconPackIcon>()
val database = AppDatabase.getInstance(context)
database.runInTransaction {
try {
val res = context.packageManager.getResourcesForApplication(pkgName)
val parser: XmlPullParser
var inStream: InputStreamReader? = null
val xmlId = res.getIdentifier("appfilter", "xml", pkgName)
if (xmlId != 0) parser = res.getXml(xmlId)
else {
val rawId = res.getIdentifier("appfilter", "raw", pkgName)
if (rawId == 0) {
Log.e(
"MM20",
"Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw"
)
return@runInTransaction
}
parser = XmlPullParserFactory.newInstance().newPullParser()
inStream = res.openRawResource(rawId).reader()
parser.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 icon = IconPackIcon(
componentName = componentName,
drawable = drawable,
iconPack = pkgName,
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 icon = IconPackIcon(
componentName = componentName,
drawable = drawable,
iconPack = pkgName,
type = "calendar"
)
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) {
iconDao.insertAll(icons.map { it.toDatabaseEntity() })
icons.clear()
}
}
if (icons.isNotEmpty()) {
iconDao.insertAll(icons.map { it.toDatabaseEntity() })
}
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)
}
}
}
private fun installGrayscaleIconMap(packageName: String) {
val database = AppDatabase.getInstance(context)
database.runInTransaction {
val iconDao = database.iconDao()
try {
val resources = context.packageManager.getResourcesForApplication(packageName)
val resId = resources.getIdentifier("grayscale_icon_map", "xml", packageName)
iconDao.deleteIcons(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.deleteIcons(packageName)
return@runInTransaction
}
}
}
}
private const val PREFERENCE_NAME = "icon_pack"
private const val KEY_ICON_PACK = "icon_pack"
private const val KEY_VERSION = "version"
private const val KEY_ICONSCALE = "iconscale"
@@ -0,0 +1,388 @@
package de.mm20.launcher2.icons
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.util.LruCache
import de.mm20.launcher2.data.customattrs.AdaptifiedLegacyIcon
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.CustomIcon
import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
import de.mm20.launcher2.data.customattrs.DefaultPlaceholderIcon
import de.mm20.launcher2.data.customattrs.ForceThemedIcon
import de.mm20.launcher2.data.customattrs.UnmodifiedSystemDefaultIcon
import de.mm20.launcher2.icons.providers.CalendarIconProvider
import de.mm20.launcher2.icons.providers.CustomIconPackIconProvider
import de.mm20.launcher2.icons.providers.CustomThemedIconProvider
import de.mm20.launcher2.icons.providers.GoogleClockIconProvider
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
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.transform
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
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.map
import kotlinx.coroutines.launch
class IconRepository(
val context: Context,
private val iconPackManager: IconPackManager,
private val dataStore: LauncherDataStore,
private val customAttributesRepository: CustomAttributesRepository,
) {
private val appReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
requestIconPackListUpdate()
}
}
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val cache = LruCache<String, LauncherIcon>(200)
private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf())
private var placeholderProvider: IconProvider? = null
private var transformations: MutableStateFlow<List<LauncherIconTransformation>> =
MutableStateFlow(
listOf()
)
init {
requestIconPackListUpdate()
context.registerReceiver(appReceiver, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
})
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.themedIcons) {
providers.add(ThemedIconProvider(iconPackManager))
}
if (settings.iconPack.isNotBlank()) {
providers.add(
IconPackIconProvider(
context,
settings.iconPack,
iconPackManager
)
)
}
providers.add(GoogleClockIconProvider(context))
providers.add(CalendarIconProvider(context))
providers.add(SystemIconProvider(context, settings.themedIcons))
providers.add(placeholderProvider)
cache.evictAll()
val transformations = mutableListOf<LauncherIconTransformation>()
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
}
}
}
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon> = channelFlow {
iconProviders.collectLatest { providers ->
transformations.collectLatest { transformations ->
customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon ->
val provs = getProviders(customIcon) + providers
val transforms = getTransformations(customIcon) ?: transformations
var icon = cache.get(searchable.key + customIcon.hashCode())
if (icon != null) {
send(icon)
return@collectLatest
}
val placeholder = placeholderProvider?.getIcon(searchable, size)
placeholder?.let { send(it) }
icon = provs.getFirstIcon(searchable, size)
if (icon != null) {
icon = icon.transform(transforms)
cache.put(searchable.key + customIcon.hashCode(), icon)
send(icon)
}
}
}
}
}
private fun getProviders(customIcon: CustomIcon?): List<IconProvider> {
if (customIcon is UnmodifiedSystemDefaultIcon) {
return listOf(
SystemIconProvider(context, false)
)
}
if (customIcon is CustomIconPackIcon) {
return listOf(
CustomIconPackIconProvider(
customIcon,
iconPackManager
)
)
}
if (customIcon is CustomThemedIcon) {
return listOf(
CustomThemedIconProvider(
customIcon,
iconPackManager
)
)
}
if (customIcon is DefaultPlaceholderIcon) {
return placeholderProvider?.let { listOf(it) } ?: emptyList()
}
return emptyList()
}
private fun getTransformations(customIcon: CustomIcon?): List<LauncherIconTransformation>? {
customIcon ?: return null
if (customIcon is AdaptifiedLegacyIcon) {
return listOf(
LegacyToAdaptiveTransformation(
foregroundScale = customIcon.fgScale,
backgroundColor = customIcon.bgColor
)
)
}
if (customIcon is ForceThemedIcon) {
return listOf(
ForceThemedIconTransformation()
)
}
if (customIcon is UnmodifiedSystemDefaultIcon) {
return emptyList()
}
return null
}
fun requestIconPackListUpdate() {
scope.launch {
iconPackManager.updateIconPacks()
}
}
suspend fun getInstalledIconPacks(): List<IconPack> {
return iconPackManager.getInstalledIconPacks()
}
suspend fun getCustomIconSuggestions(
searchable: SavableSearchable,
size: Int
): List<CustomIconWithPreview> {
val suggestions = mutableListOf<CustomIconWithPreview>()
val rawIcon = iconProviders.first().getFirstIcon(searchable, size) ?: return emptyList()
val defaultTransformations = transformations.first()
val transformationOptions = mutableListOf<CustomIcon>(UnmodifiedSystemDefaultIcon)
if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) {
// Legacy icons that simply fill the entire canvas
transformationOptions.add(
AdaptifiedLegacyIcon(
fgScale = 1f,
bgColor = 1
)
)
// 48x48 with 5px padding used to be the default icon size for icons generated by
// the Android Studio asset generator. Upscale these icons to remove that padding.
transformationOptions.add(
AdaptifiedLegacyIcon(
fgScale = 48f / 38f,
bgColor = 1
)
)
// Android 7.1 round icons (48x48 circle with 1px padding)
transformationOptions.add(
AdaptifiedLegacyIcon(
fgScale = 48f / 44f,
bgColor = 1
)
)
transformationOptions.add(
AdaptifiedLegacyIcon(
fgScale = 0.7f,
bgColor = 0
)
)
transformationOptions.add(
AdaptifiedLegacyIcon(
fgScale = 0.7f,
bgColor = Color.WHITE,
)
)
}
val providerOptions = mutableListOf<CustomIcon>()
if (searchable is LauncherApp) {
val iconPackIcons = iconPackManager.getAllIconPackIcons(
searchable.launcherActivityInfo.componentName
)
providerOptions.addAll(
iconPackIcons.mapNotNull {
CustomIconPackIcon(
iconPackPackage = it.iconPack,
iconComponentName = it.componentName?.flattenToString()
?: return@mapNotNull null
)
}
)
val themedIcon = iconPackManager.getGreyscaleIcon(searchable.`package`)
if (themedIcon != null && themedIcon.componentName?.packageName != null) {
providerOptions.add(
CustomThemedIcon(
iconPackageName = themedIcon.componentName.packageName,
)
)
} else {
transformationOptions.add(
ForceThemedIcon
)
}
} else {
transformationOptions.add(
ForceThemedIcon
)
}
providerOptions.add(DefaultPlaceholderIcon)
suggestions.addAll(
transformationOptions.map {
val transformations = getTransformations(it) ?: defaultTransformations
val providers = getProviders(it)
val icon = providers.getFirstIcon(searchable, size) ?: rawIcon
CustomIconWithPreview(
preview = icon.transform(transformations),
customIcon = it,
)
}
)
suggestions.addAll(
providerOptions.mapNotNull {
val providers = getProviders(it)
val icon = providers.getFirstIcon(searchable, size) ?: return@mapNotNull null
CustomIconWithPreview(
preview = icon.transform(defaultTransformations),
customIcon = it,
)
}
)
return suggestions
}
suspend fun getUncustomizedDefaultIcon(
searchable: SavableSearchable,
size: Int
): CustomIconWithPreview? {
val icon = iconProviders.first().getFirstIcon(searchable, size)
?.transform(transformations.first()) ?: return null
return CustomIconWithPreview(
customIcon = null,
preview = icon
)
}
suspend fun searchCustomIcons(query: String): List<CustomIconWithPreview> {
val transformations = this.transformations.first()
val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview(
customIcon = CustomIconPackIcon(
iconPackPackage = it.iconPack,
iconComponentName = componentName.flattenToString(),
),
preview = iconPackManager.getIcon(it.iconPack, componentName)
?.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
}
fun setCustomIcon(searchable: SavableSearchable, icon: CustomIcon?) {
customAttributesRepository.setCustomIcon(searchable, icon)
}
}
data class CustomIconWithPreview(
val preview: LauncherIcon,
val customIcon: CustomIcon?,
)
@@ -0,0 +1,9 @@
package de.mm20.launcher2.icons
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val iconsModule = module {
single { IconPackManager(androidContext(), get()) }
single { IconRepository(androidContext(), get(), get(), get()) }
}
@@ -0,0 +1,7 @@
package de.mm20.launcher2.icons
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
internal interface TransformableDynamicLauncherIcon {
fun setTransformations(transformations: List<LauncherIconTransformation>)
}
@@ -0,0 +1,42 @@
package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import de.mm20.launcher2.icons.DynamicCalendarIcon
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
class CalendarIconProvider(val context: Context): IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
if(searchable !is LauncherApp) return null
val component = ComponentName(searchable.`package`, searchable.activity)
val pm = context.packageManager
val ai = try {
pm.getActivityInfo(component, PackageManager.GET_META_DATA)
} catch (e: PackageManager.NameNotFoundException) {
return null
}
val resources = pm.getResourcesForActivity(component)
var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0
if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons")
?: return null
if (arrayId == 0) return null
val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null
if (typedArray.length() != 31) {
typedArray.recycle()
return null
}
val drawableIds = IntArray(31)
for (i in 0 until 31) {
drawableIds[i] = typedArray.getResourceId(i, 0)
}
typedArray.recycle()
return DynamicCalendarIcon(
resources = resources,
resourceIds = drawableIds
)
}
}
@@ -0,0 +1,19 @@
package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
class CustomIconPackIconProvider(
private val customIcon: CustomIconPackIcon,
private val iconPackManager: IconPackManager,
) : IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
return iconPackManager.getIcon(
customIcon.iconPackPackage,
ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null
)
}
}
@@ -0,0 +1,15 @@
package de.mm20.launcher2.icons.providers
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
class CustomThemedIconProvider(
private val customIcon: CustomThemedIcon,
private val iconPackManager: IconPackManager,
): IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
}
}
@@ -0,0 +1,87 @@
package de.mm20.launcher2.icons.providers
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
class GoogleClockIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null
if (searchable.`package` != "com.google.android.deskclock") return null
val pm = context.packageManager
val appInfo = try {
pm.getApplicationInfo(
"com.google.android.deskclock",
PackageManager.GET_META_DATA
)
} catch (e: PackageManager.NameNotFoundException) {
return null
}
val drawable =
appInfo.metaData.getInt("com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND")
val resources = pm.getResourcesForApplication(appInfo)
val baseIcon = try {
ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable
?: return null
} catch (e: Resources.NotFoundException) {
return null
}
val foreground = baseIcon.foreground as? LayerDrawable ?: return null
val hourLayer =
appInfo.metaData.getInt("com.android.launcher3.HOUR_LAYER_INDEX")
val minuteLayer =
appInfo.metaData.getInt("com.android.launcher3.MINUTE_LAYER_INDEX")
val secondLayer =
appInfo.metaData.getInt("com.android.launcher3.SECOND_LAYER_INDEX")
val defaultHour =
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_HOUR")
val defaultMinute =
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_MINUTE")
val defaultSecond =
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_SECOND")
return StaticLauncherIcon(
foregroundLayer = ClockLayer(
sublayers = (0 until foreground.numberOfLayers).map {
val drw = foreground.getDrawable(it)
if (drw is RotateDrawable) {
drw.level = when (it) {
hourLayer -> {
(12 - defaultHour) * 60
}
minuteLayer -> {
(60 - defaultMinute)
}
secondLayer -> {
(60 - defaultSecond) * 10
}
else -> 0
}
}
ClockSublayer(
drawable = drw,
role = when (it) {
hourLayer -> ClockSublayerRole.Hour
minuteLayer -> ClockSublayerRole.Minute
secondLayer -> ClockSublayerRole.Second
else -> ClockSublayerRole.Static
}
)
},
scale = 1.5f,
),
backgroundLayer = StaticIconLayer(
icon = baseIcon.background,
scale = 1.5f,
)
)
}
}
@@ -0,0 +1,30 @@
package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import android.content.Context
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class IconPackIconProvider(
private val context: Context,
private val iconPack: String,
private val iconPackManager: IconPackManager,
): IconProvider {
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, component)
?: iconPackManager.generateIcon(
context,
iconPack,
baseIcon = withContext(Dispatchers.IO) {
searchable.launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)
},
size = size,
)
}
}
@@ -0,0 +1,21 @@
package de.mm20.launcher2.icons.providers
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
interface IconProvider {
suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon?
}
internal suspend fun Iterable<IconProvider>.getFirstIcon(
searchable: SavableSearchable,
size: Int
): LauncherIcon? {
for (provider in this) {
val icon = provider.getIcon(searchable, size)
if (icon != null) {
return icon
}
}
return null
}
@@ -0,0 +1,11 @@
package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
class PlaceholderIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon {
return searchable.getPlaceholderIcon(context)
}
}
@@ -0,0 +1,14 @@
package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
class SystemIconProvider(
private val context: Context,
private val themedIcons: Boolean,
) : IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
return searchable.loadIcon(context, size, themedIcons)
}
}
@@ -0,0 +1,15 @@
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`)
}
}
@@ -0,0 +1,40 @@
package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.SavableSearchable
internal class ThemedPlaceholderIconProvider(
private val context: Context,
) : IconProvider {
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon {
val icon = searchable.getPlaceholderIcon(context)
return StaticLauncherIcon(
foregroundLayer = asThemed(icon.foregroundLayer),
backgroundLayer = asThemed(icon.backgroundLayer),
)
}
private fun asThemed(layer: LauncherIconLayer): LauncherIconLayer {
return when (layer) {
is ClockLayer -> TintedClockLayer(
scale = layer.scale,
color = 0,
sublayers = layer.sublayers,
)
is ColorLayer -> layer.copy(color = 0)
is StaticIconLayer -> TintedIconLayer(
icon = layer.icon,
color = 0,
scale = layer.scale,
)
is TextLayer -> layer.copy(color = 0)
is TintedIconLayer -> layer.copy(color = 0)
is TintedClockLayer -> return layer.copy(color = 0)
is TransparentLayer -> return layer
}
}
}
@@ -0,0 +1,32 @@
package de.mm20.launcher2.icons.transformations
import de.mm20.launcher2.icons.*
internal class ForceThemedIconTransformation : LauncherIconTransformation {
override suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = asThemed(icon.foregroundLayer),
backgroundLayer = ColorLayer(0),
)
}
private fun asThemed(layer: LauncherIconLayer): LauncherIconLayer {
return when(layer) {
is ClockLayer -> TintedClockLayer(
scale = layer.scale,
sublayers = layer.sublayers,
)
is ColorLayer -> layer.copy(color = 0)
is StaticIconLayer -> TintedIconLayer(
color = 0,
icon = layer.icon,
scale = layer.scale / 1.5f,
)
is TextLayer -> layer.copy(
color = 0
)
else -> layer
}
}
}
@@ -0,0 +1,24 @@
package de.mm20.launcher2.icons.transformations
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TransformableDynamicLauncherIcon
internal interface LauncherIconTransformation {
suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon
}
internal suspend fun LauncherIcon.transform(transformations: Iterable<LauncherIconTransformation>): LauncherIcon {
if (this is StaticLauncherIcon) {
var transformedIcon = this
for (transformation in transformations) {
transformedIcon = transformation.transform(transformedIcon as StaticLauncherIcon)
}
return transformedIcon
}
if (this is TransformableDynamicLauncherIcon) {
this.setTransformations(transformations.toList())
return this
}
return this
}
@@ -0,0 +1,53 @@
package de.mm20.launcher2.icons.transformations
import android.graphics.drawable.BitmapDrawable
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import de.mm20.launcher2.icons.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class LegacyToAdaptiveTransformation(
private val foregroundScale: Float = 0.7f,
private val backgroundColor: Int = 1,
): LauncherIconTransformation {
override suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon {
if (icon.backgroundLayer !is TransparentLayer) return icon
val bgColor = if (backgroundColor == 1) extractColor(icon.foregroundLayer) else backgroundColor
return StaticLauncherIcon(
foregroundLayer = scale(icon.foregroundLayer, foregroundScale),
backgroundLayer = ColorLayer(bgColor)
)
}
private fun scale(layer: LauncherIconLayer, scale: Float): LauncherIconLayer {
return when(layer) {
is ClockLayer -> layer.copy(scale = scale)
is StaticIconLayer -> layer.copy(scale = scale)
is TintedClockLayer -> layer.copy(scale = scale)
is TintedIconLayer -> layer.copy(scale = scale)
else -> layer
}
}
private suspend fun extractColor(layer: LauncherIconLayer): Int {
if (layer is StaticIconLayer) {
val drawable = layer.icon
val bitmap = if (drawable is BitmapDrawable) {
drawable.bitmap
} else {
drawable.toBitmap(48, 48)
}
val palette = withContext(Dispatchers.Default) {
Palette.from(bitmap).generate()
}
return palette.getDominantColor(0)
} else if (layer is ColorLayer) {
return layer.color
}
return 0
}
}