From 59b8f17fc47950f7429982d7522bb63d806af820 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:23:17 +0100 Subject: [PATCH] Compat themed icons - Use native themed icons on Android 8.0 to 12 --- .../de/mm20/launcher2/database/IconDao.kt | 17 +- .../de/mm20/launcher2/ktx/XmlPullParser.kt | 10 + .../mm20/launcher2/icons/IconPackManager.kt | 350 +++--------------- .../de/mm20/launcher2/icons/IconRepository.kt | 12 +- .../java/de/mm20/launcher2/icons/Module.kt | 1 + .../icons/compat/ThemedIconCompat.kt | 26 ++ .../loaders/CompatThemedIconInstaller.kt | 109 ++++++ .../icons/loaders/GrayscaleMapInstaller.kt | 88 +++++ .../icons/loaders/IconPackInstaller.kt | 231 ++++++++++++ .../providers/CompatThemedIconProvider.kt | 17 + 10 files changed, 557 insertions(+), 304 deletions(-) create mode 100644 core/ktx/src/main/java/de/mm20/launcher2/ktx/XmlPullParser.kt create mode 100644 services/icons/src/main/java/de/mm20/launcher2/icons/compat/ThemedIconCompat.kt create mode 100644 services/icons/src/main/java/de/mm20/launcher2/icons/loaders/CompatThemedIconInstaller.kt create mode 100644 services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt create mode 100644 services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt create mode 100644 services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatThemedIconProvider.kt diff --git a/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt index 7920edb2..b1871f84 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt @@ -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 @@ -41,7 +44,7 @@ interface IconDao { @Transaction suspend fun installGrayscaleIconMap(packageName: String, icons: List) { - 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) + @Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs) AND (type = 'calendar' OR type = 'app')") + fun deleteAllIconsPackIconsExcept(packs: List) + + @Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs) AND type = 'greyscale_icon'") + fun deleteAllGrayscaleIconsExcept(packs: List) @Query("DELETE FROM IconPack WHERE packageName NOT IN (:packs)") fun deleteAllPacksExcept(packs: List) + @Query("DELETE FROM Icons WHERE type = 'themed-compat'") + fun deleteAllCompatThemedIcons() + @Transaction fun uninstallIconPacksExcept(packs: List) { - deleteAllIconsExcept(packs) + deleteAllIconsPackIconsExcept(packs) deleteAllPacksExcept(packs) } diff --git a/core/ktx/src/main/java/de/mm20/launcher2/ktx/XmlPullParser.kt b/core/ktx/src/main/java/de/mm20/launcher2/ktx/XmlPullParser.kt new file mode 100644 index 00000000..b2041adf --- /dev/null +++ b/core/ktx/src/main/java/de/mm20/launcher2/ktx/XmlPullParser.kt @@ -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 +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt index 7b18f29f..e4fac416 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt @@ -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 { 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 { - 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 { - val packs = mutableListOf() - 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() - 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() - 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" diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt index 83fd0c33..1901fd94 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -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() - 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() diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/Module.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/Module.kt index e8fb6ae5..c599d1b3 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/Module.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/Module.kt @@ -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 diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/compat/ThemedIconCompat.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/compat/ThemedIconCompat.kt new file mode 100644 index 00000000..4fcbf008 --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/compat/ThemedIconCompat.kt @@ -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, +) { + + +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/CompatThemedIconInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/CompatThemedIconInstaller.kt new file mode 100644 index 00000000..8c699568 --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/CompatThemedIconInstaller.kt @@ -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() + 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 { + val resolveInfos = context.packageManager.queryIntentActivities( + Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 0 + ) + + return resolveInfos.mapNotNull { it.activityInfo } + } +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt new file mode 100644 index 00000000..987894f2 --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/GrayscaleMapInstaller.kt @@ -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 { + 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() + 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 + } + + } + } +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt new file mode 100644 index 00000000..103590e3 --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/loaders/IconPackInstaller.kt @@ -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 { + val packs = mutableListOf() + 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() + 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) + } + + } + } + + +} \ No newline at end of file diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatThemedIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatThemedIconProvider.kt new file mode 100644 index 00000000..878f7630 --- /dev/null +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatThemedIconProvider.kt @@ -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) + } +} \ No newline at end of file