Compat themed icons
- Use native themed icons on Android 8.0 to 12
This commit is contained in:
parent
9e56f960e6
commit
59b8f17fc4
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
|
||||
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user