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")
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')")
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
@ -41,7 +44,7 @@ interface IconDao {
@Transaction
suspend fun installGrayscaleIconMap(packageName: String, icons: List<IconEntity>) {
deleteIcons(packageName)
deleteGrayscaleIcons(packageName)
insertAll(icons)
}
@ -68,15 +71,21 @@ interface IconDao {
return getPacks(iconPack.packageName, iconPack.version).isNotEmpty()
}
@Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs)")
fun deleteAllIconsExcept(packs: List<String>)
@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>)
@Query("DELETE FROM Icons WHERE type = 'themed-compat'")
fun deleteAllCompatThemedIcons()
@Transaction
fun uninstallIconPacksExcept(packs: List<String>) {
deleteAllIconsExcept(packs)
deleteAllIconsPackIconsExcept(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.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.graphics.Bitmap
import android.graphics.Canvas
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 androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.loaders.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.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.IOException
import java.io.Reader
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,
@ -57,11 +53,17 @@ class IconPackManager(
suspend fun updateIconPacks() {
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 {
context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) {
@ -104,6 +106,7 @@ class IconPackManager(
)
}
}
drawable is AdaptiveIconDrawable -> {
return StaticLauncherIcon(
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> {
val iconDao = appDatabase.iconDao()
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.UnmodifiedSystemDefaultIcon
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.CustomThemedIconProvider
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.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.transform
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
@ -90,10 +92,6 @@ class IconRepository(
}
val providers = mutableListOf<IconProvider>()
if (settings.themedIcons) {
providers.add(ThemedIconProvider(iconPackManager))
}
if (settings.iconPack.isNotBlank()) {
val pack = iconPackManager.getIconPack(settings.iconPack)
if (pack != null) {
@ -110,6 +108,12 @@ class IconRepository(
}
providers.add(GoogleClockIconProvider(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(placeholderProvider)
cache.evictAll()

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.icons
import de.mm20.launcher2.icons.compat.ThemedIconsCompatManager
import org.koin.android.ext.koin.androidContext
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)
}
}