Compat themed icons

- Use native themed icons on Android 8.0 to 12
This commit is contained in:
MM20 2023-02-15 15:23:17 +01:00
parent 9e56f960e6
commit 59b8f17fc4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
10 changed files with 557 additions and 304 deletions

View File

@ -16,6 +16,9 @@ interface IconDao {
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack AND (type = 'app' OR type = 'calendar') LIMIT 1") @Query("SELECT * FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack AND (type = 'app' OR type = 'calendar') LIMIT 1")
suspend fun getIcon(componentName: String, iconPack: String): IconEntity? suspend fun getIcon(componentName: String, iconPack: String): IconEntity?
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'themed-compat') LIMIT 1")
suspend fun getCompatThemedIcon(componentName: String): IconEntity?
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')") @Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity> suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
@ -41,7 +44,7 @@ interface IconDao {
@Transaction @Transaction
suspend fun installGrayscaleIconMap(packageName: String, icons: List<IconEntity>) { suspend fun installGrayscaleIconMap(packageName: String, icons: List<IconEntity>) {
deleteIcons(packageName) deleteGrayscaleIcons(packageName)
insertAll(icons) insertAll(icons)
} }
@ -68,15 +71,21 @@ interface IconDao {
return getPacks(iconPack.packageName, iconPack.version).isNotEmpty() return getPacks(iconPack.packageName, iconPack.version).isNotEmpty()
} }
@Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs)") @Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs) AND (type = 'calendar' OR type = 'app')")
fun deleteAllIconsExcept(packs: List<String>) 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)") @Query("DELETE FROM IconPack WHERE packageName NOT IN (:packs)")
fun deleteAllPacksExcept(packs: List<String>) fun deleteAllPacksExcept(packs: List<String>)
@Query("DELETE FROM Icons WHERE type = 'themed-compat'")
fun deleteAllCompatThemedIcons()
@Transaction @Transaction
fun uninstallIconPacksExcept(packs: List<String>) { fun uninstallIconPacksExcept(packs: List<String>) {
deleteAllIconsExcept(packs) deleteAllIconsPackIconsExcept(packs)
deleteAllPacksExcept(packs) deleteAllPacksExcept(packs)
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.ktx
import org.xmlpull.v1.XmlPullParser
fun XmlPullParser.skipToNextTag(): Boolean {
while (next() != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) return true
}
return false
}

View File

@ -2,38 +2,34 @@ package de.mm20.launcher2.icons
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.res.Resources import android.content.res.Resources
import android.content.res.XmlResourceParser import android.graphics.Bitmap
import android.graphics.* import android.graphics.Canvas
import android.graphics.drawable.* import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
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.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.loaders.CompatThemedIconInstaller
import de.mm20.launcher2.icons.loaders.GrayscaleMapInstaller
import de.mm20.launcher2.icons.loaders.IconPackInstaller
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.withContext 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
import kotlin.math.roundToInt 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( class IconPackManager(
private val context: Context, private val context: Context,
@ -57,11 +53,17 @@ class IconPackManager(
suspend fun updateIconPacks() { suspend fun updateIconPacks() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
UpdateIconPacksWorker(context).doWork() IconPackInstaller(context, appDatabase).installIcons()
GrayscaleMapInstaller(context, appDatabase).installIcons()
CompatThemedIconInstaller(context, appDatabase).installIcons()
} }
} }
suspend fun getIcon(iconPack: String, componentName: ComponentName, themed: Boolean = false): LauncherIcon? { suspend fun getIcon(
iconPack: String,
componentName: ComponentName,
themed: Boolean = false
): LauncherIcon? {
val res = try { val res = try {
context.packageManager.getResourcesForApplication(iconPack) context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
@ -104,6 +106,7 @@ class IconPackManager(
) )
} }
} }
drawable is AdaptiveIconDrawable -> { drawable is AdaptiveIconDrawable -> {
return StaticLauncherIcon( return StaticLauncherIcon(
foregroundLayer = drawable.foreground?.let { foregroundLayer = drawable.foreground?.let {
@ -233,6 +236,37 @@ class IconPackManager(
) )
} }
suspend fun getCompatThemedIcon(componentName: ComponentName): LauncherIcon? {
val iconDao = appDatabase.iconDao()
val icon = iconDao.getCompatThemedIcon(componentName.flattenToString())
?: return null
val drawableName = icon.drawable ?: return null
val res = try {
context.packageManager.getResourcesForApplication(componentName.packageName)
} catch (e: Resources.NotFoundException) {
return null
} catch (e: PackageManager.NameNotFoundException) {
return null
}
val resourceId = res.getIdentifier(drawableName, null, null)
val drawable = try {
ResourcesCompat.getDrawable(res, resourceId, null)
} catch (e: Resources.NotFoundException) {
return null
} ?: return null
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = drawable,
scale = 1f,
),
backgroundLayer = ColorLayer()
)
}
suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackIcon> { suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackIcon> {
val iconDao = appDatabase.iconDao() val iconDao = appDatabase.iconDao()
return iconDao.getIconsFromAllPacks(componentName.flattenToString()) return iconDao.getIconsFromAllPacks(componentName.flattenToString())
@ -443,279 +477,3 @@ class IconPackManager(
} }
class UpdateIconPacksWorker(val context: Context) {
fun doWork() {
val packs = loadInstalledPacks(context)
val grayscaleProviders = loadInstalledGreyscaleProviders(context)
val iconDao = AppDatabase.getInstance(context).iconDao()
iconDao.uninstallIconPacksExcept(
packs.map { it.packageName }.union(grayscaleProviders).toList()
)
for (pack in packs) {
try {
installIconPack(pack)
} 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<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>()
val database = AppDatabase.getInstance(context)
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,
type = "app"
)
icons.add(icon)
}
"calendar" -> {
val component = parser.getAttributeValue(null, "component")
?: continue@loop
val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop
if (component.length < 14) continue@loop
val componentName = ComponentName.unflattenFromString(
component.substring(
14,
component.lastIndex
)
)
?: continue@loop
val name = parser.getAttributeValue(null, "name")
val icon = IconPackIcon(
componentName = componentName,
drawable = drawable,
iconPack = pkgName,
type = "calendar",
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) {
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.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
}
}
}
}
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"

View File

@ -16,6 +16,7 @@ import de.mm20.launcher2.data.customattrs.DefaultPlaceholderIcon
import de.mm20.launcher2.data.customattrs.ForceThemedIcon import de.mm20.launcher2.data.customattrs.ForceThemedIcon
import de.mm20.launcher2.data.customattrs.UnmodifiedSystemDefaultIcon import de.mm20.launcher2.data.customattrs.UnmodifiedSystemDefaultIcon
import de.mm20.launcher2.icons.providers.CalendarIconProvider import de.mm20.launcher2.icons.providers.CalendarIconProvider
import de.mm20.launcher2.icons.providers.CompatThemedIconProvider
import de.mm20.launcher2.icons.providers.CustomIconPackIconProvider import de.mm20.launcher2.icons.providers.CustomIconPackIconProvider
import de.mm20.launcher2.icons.providers.CustomThemedIconProvider import de.mm20.launcher2.icons.providers.CustomThemedIconProvider
import de.mm20.launcher2.icons.providers.GoogleClockIconProvider import de.mm20.launcher2.icons.providers.GoogleClockIconProvider
@ -30,6 +31,7 @@ import de.mm20.launcher2.icons.transformations.ForceThemedIconTransformation
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.transform import de.mm20.launcher2.icons.transformations.transform
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
@ -90,10 +92,6 @@ class IconRepository(
} }
val providers = mutableListOf<IconProvider>() val providers = mutableListOf<IconProvider>()
if (settings.themedIcons) {
providers.add(ThemedIconProvider(iconPackManager))
}
if (settings.iconPack.isNotBlank()) { if (settings.iconPack.isNotBlank()) {
val pack = iconPackManager.getIconPack(settings.iconPack) val pack = iconPackManager.getIconPack(settings.iconPack)
if (pack != null) { if (pack != null) {
@ -110,6 +108,12 @@ class IconRepository(
} }
providers.add(GoogleClockIconProvider(context)) providers.add(GoogleClockIconProvider(context))
providers.add(CalendarIconProvider(context)) providers.add(CalendarIconProvider(context))
if (settings.themedIcons) {
if (!isAtLeastApiLevel(33)) {
providers.add(CompatThemedIconProvider(iconPackManager))
}
providers.add(ThemedIconProvider(iconPackManager))
}
providers.add(SystemIconProvider(context, settings.themedIcons)) providers.add(SystemIconProvider(context, settings.themedIcons))
providers.add(placeholderProvider) providers.add(placeholderProvider)
cache.evictAll() cache.evictAll()

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.icons package de.mm20.launcher2.icons
import de.mm20.launcher2.icons.compat.ThemedIconsCompatManager
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module

View File

@ -0,0 +1,26 @@
package de.mm20.launcher2.icons.compat
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.util.Log
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.icons.IconPackIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.skipToNextTag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
internal class ThemedIconsCompatManager(
private val context: Context,
) {
}

View File

@ -0,0 +1,109 @@
package de.mm20.launcher2.icons.loaders
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Resources
import android.content.res.XmlResourceParser
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.IconPackIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.skipToNextTag
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
class CompatThemedIconInstaller(
private val context: Context,
private val database: AppDatabase,
) {
fun installIcons() {
if (isAtLeastApiLevel(33)) return
val launcherActivities = getLauncherActivities()
val dao = database.iconDao()
val icons = mutableListOf<IconPackIcon>()
database.runInTransaction {
dao.deleteAllCompatThemedIcons()
for (activity in launcherActivities) {
val componentName = ComponentName(activity.applicationInfo.packageName, activity.name)
val monochromeIcon = getMonochromeIconResource(activity)
if (monochromeIcon != null) {
val icon = IconPackIcon(
type = "themed-compat",
componentName = componentName,
name = null,
drawable = monochromeIcon,
iconPack = componentName.packageName
)
icons.add(icon)
}
if (icons.size > 100) {
dao.insertAll(icons.map { it.toDatabaseEntity() })
icons.clear()
}
}
if (icons.isNotEmpty()) {
dao.insertAll(icons.map { it.toDatabaseEntity() })
}
}
}
private fun getMonochromeIconResource(activityInfo: ActivityInfo): String? {
val iconResource = activityInfo.iconResource
val resources = try {
context.packageManager.getResourcesForApplication(activityInfo.packageName)
} catch (e: PackageManager.NameNotFoundException) {
CrashReporter.logException(e)
return null
}
var xmlParser: XmlResourceParser? = null
try {
xmlParser = resources.getXml(iconResource)
if (!xmlParser.skipToNextTag()) return null
if (xmlParser.name != "adaptive-icon") {
return null
}
while (xmlParser.skipToNextTag()) {
if (xmlParser.name == "monochrome") {
val drawable = xmlParser.getAttributeResourceValue(
"http://schemas.android.com/apk/res/android",
"drawable",
0
)
if (drawable == 0) return null
return resources.getResourceName(drawable)
}
}
} catch (e: Resources.NotFoundException) {
CrashReporter.logException(e)
return null
} catch (e: IOException) {
CrashReporter.logException(e)
return null
} catch (e: XmlPullParserException) {
CrashReporter.logException(e)
return null
} finally {
xmlParser?.close()
}
return null
}
private fun getLauncherActivities(): List<ActivityInfo> {
val resolveInfos = context.packageManager.queryIntentActivities(
Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 0
)
return resolveInfos.mapNotNull { it.activityInfo }
}
}

View File

@ -0,0 +1,88 @@
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

@ -0,0 +1,231 @@
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.IconPack
import de.mm20.launcher2.icons.IconPackIcon
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.Reader
class IconPackInstaller(
private val context: Context,
private val database: AppDatabase,
) {
fun installIcons() {
val packs = loadInstalledPacks(context)
val iconDao = database.iconDao()
for (pack in packs) {
try {
installIconPack(pack)
} catch (e: PackageManager.NameNotFoundException) {
continue
}
}
iconDao.uninstallIconPacksExcept(
packs.map { it.packageName }.toList()
)
}
private fun loadInstalledPacks(context: Context): List<IconPack> {
val packs = mutableListOf<IconPack>()
val pm = context.packageManager
var intent = Intent("app.lawnchair.icons.THEMED_ICON")
val themedPacks = pm.queryIntentActivities(intent, 0)
packs.addAll(themedPacks.map { IconPack(context, it, true) })
intent = Intent("org.adw.ActivityStarter.THEMES")
val adwPacks = pm.queryIntentActivities(intent, 0)
packs.addAll(adwPacks.map { IconPack(context, it, false) })
intent = Intent("com.novalauncher.THEME")
val novaPacks = pm.queryIntentActivities(intent, 0)
packs.addAll(novaPacks.map { IconPack(context, it, false) })
return packs.distinctBy { it.packageName }
}
private fun installIconPack(iconPack: IconPack) {
val pkgName = iconPack.packageName
val icons = mutableListOf<IconPackIcon>()
database.runInTransaction {
try {
val res = context.packageManager.getResourcesForApplication(pkgName)
val parser: XmlPullParser
var inStream: Reader? = null
val xmlId = res.getIdentifier("appfilter", "xml", pkgName)
val rawId = res.getIdentifier("appfilter", "raw", pkgName)
parser = when {
xmlId != 0 -> res.getXml(xmlId)
rawId != 0 -> {
inStream = res.openRawResource(rawId).reader()
XmlPullParserFactory.newInstance().newPullParser().apply {
setInput(inStream)
}
}
else -> {
val iconPackContext = context.createPackageContext(
pkgName,
Context.CONTEXT_IGNORE_SECURITY
)
inStream = try {
iconPackContext.assets.open("appfilter.xml").reader()
} catch (e: IOException) {
CrashReporter.logException(e)
Log.e(
"MM20",
"appfilter.xml not found in $pkgName. Searched locations: res/xml/appfilter.xml, res/raw/appfilter.xml, assets/appfilter.xml"
)
return@runInTransaction
}
XmlPullParserFactory.newInstance().newPullParser().apply {
setInput(inStream)
}
}
}
val iconDao = database.iconDao()
iconDao.deleteIconPack(iconPack.toDatabaseEntity())
iconDao.deleteIcons(iconPack.packageName)
loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.eventType != XmlPullParser.START_TAG) continue
when (parser.name) {
"item" -> {
val component = parser.getAttributeValue(null, "component")
?: continue@loop
val drawable = parser.getAttributeValue(null, "drawable")
?: continue@loop
if (component.length <= 14) continue@loop
val componentName = ComponentName.unflattenFromString(
component.substring(
14,
component.lastIndex
)
)
?: continue@loop
val name = parser.getAttributeValue(null, "name")
val icon = IconPackIcon(
componentName = componentName,
drawable = drawable,
iconPack = pkgName,
name = name,
type = "app"
)
icons.add(icon)
}
"calendar" -> {
val component = parser.getAttributeValue(null, "component")
?: continue@loop
val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop
if (component.length < 14) continue@loop
val componentName = ComponentName.unflattenFromString(
component.substring(
14,
component.lastIndex
)
)
?: continue@loop
val name = parser.getAttributeValue(null, "name")
val icon = IconPackIcon(
componentName = componentName,
drawable = drawable,
iconPack = pkgName,
type = "calendar",
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) {
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)
}
}
}
}

View File

@ -0,0 +1,17 @@
package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
class CompatThemedIconProvider(
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.getCompatThemedIcon(component)
}
}