Add themed icons to icon picker, add option to force themed icons

This commit is contained in:
MM20 2022-07-31 21:44:10 +02:00
parent b979c81501
commit 389b33aebe
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
12 changed files with 260 additions and 143 deletions

View File

@ -81,8 +81,7 @@ sealed class CustomIcon : CustomAttribute {
}
"custom_themed_icon" -> {
CustomThemedIcon(
iconName = payload.getString("icon"),
iconPackPackage = payload.getString("icon_pack")
iconPackageName = payload.getString("icon"),
)
}
"default_icon" -> {
@ -143,14 +142,12 @@ data class AdaptifiedLegacyIcon(
}
data class CustomThemedIcon(
val iconPackPackage: String,
val iconName: String,
val iconPackageName: String,
) : CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "custom_themed_icon",
"icon" to iconName,
"icon_pack" to iconPackPackage,
"icon" to iconPackageName,
).toString()
}
}

View File

@ -19,9 +19,12 @@ interface IconDao {
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND drawable LIKE :query ORDER BY iconPack, drawable LIMIT :limit")
@Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :query OR componentName LIKE :query) ORDER BY iconPack, drawable LIMIT :limit")
suspend fun searchIconPackIcons(query: String, limit: Int = 100): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'greyscale_icon') AND componentName LIKE :query GROUP BY componentName ORDER BY drawable LIMIT :limit")
suspend fun searchGreyscaleIcons(query: String, limit: Int = 100): List<IconEntity>
@Query("DELETE FROM Icons WHERE iconPack = :iconPack")
fun deleteIcons(iconPack: String)

View File

@ -454,6 +454,8 @@
<string name="preference_theme_system">Follow system</string>
<string name="preference_themed_icons">Themed icons</string>
<string name="preference_themed_icons_summary">Color icons with the application\'s color scheme</string>
<string name="preference_force_themed_icons">Force themed icons</string>
<string name="preference_force_themed_icons_summary">Apply the application\'s color scheme to all icons, including unsupported ones (not recommended)</string>
<string name="preference_icon_pack">Icon pack</string>
<string name="preference_icon_pack_summary_empty">No icon packs installed</string>
<string name="preference_category_system_bars">System bars</string>

View File

@ -8,15 +8,14 @@ import android.content.pm.ResolveInfo
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.*
import android.util.Log
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.customattrs.CustomIconPackIcon
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.ktx.randomElementOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -237,6 +236,138 @@ class IconPackManager(
)
}
suspend fun getThemedIcon(packageName: String): LauncherIcon? {
val icon = getGreyscaleIcon(packageName) ?: return null
val resId = icon.drawable?.toIntOrNull() ?: return null
try {
val resources = context.packageManager.getResourcesForApplication(icon.iconPack)
return getThemedClockIcon(resources, resId) ?: getThemedCalendarIcon(
resources,
resId,
iconProviderPackage = icon.iconPack
) ?: getThemedStaticIcon(resources, resId)
} catch (e: PackageManager.NameNotFoundException) {
CrashReporter.logException(e)
}
return null
}
suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? {
val iconDao = AppDatabase.getInstance(context).iconDao()
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
?.let { IconPackIcon(it) }
}
private fun getThemedStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
try {
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = fg,
scale = 0.5f,
),
backgroundLayer = ColorLayer()
)
} catch (e: Resources.NotFoundException) {
return null
}
}
private fun getThemedClockIcon(resources: Resources, resId: Int): LauncherIcon? {
try {
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
var i = 0
var drawable: LayerDrawable? = null
var minuteIndex: Int? = null
var defaultMinute = 0
var hourIndex: Int? = null
var defaultHour = 0
while (i < array.length()) {
when (array.getString(i)) {
"com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> {
i++
drawable = array.getDrawable(i) as? LayerDrawable
}
"com.android.launcher3.HOUR_LAYER_INDEX" -> {
i++
hourIndex = array.getInt(i, -1).takeIf { it != -1 }
}
"com.android.launcher3.MINUTE_LAYER_INDEX" -> {
i++
minuteIndex = array.getInt(i, -1).takeIf { it != -1 }
}
"com.android.launcher3.DEFAULT_HOUR" -> {
i++
defaultHour = array.getInt(i, 0)
}
"com.android.launcher3.DEFAULT_MINUTE" -> {
i++
defaultMinute = array.getInt(i, 0)
}
}
i++
}
if (drawable != null && minuteIndex != null && hourIndex != null) {
return StaticLauncherIcon(
foregroundLayer = TintedClockLayer(
sublayers = (0 until drawable.numberOfLayers).map {
val drw = drawable.getDrawable(it)
if (drw is RotateDrawable) {
drw.level = when (it) {
hourIndex -> {
(12 - defaultHour) * 60
}
minuteIndex -> {
(60 - defaultMinute)
}
else -> 0
}
}
ClockSublayer(
drawable = drw,
role = when (it) {
hourIndex -> ClockSublayerRole.Hour
minuteIndex -> ClockSublayerRole.Minute
else -> ClockSublayerRole.Static
}
)
},
scale = 1.5f,
),
backgroundLayer = ColorLayer()
)
}
} catch (e: Resources.NotFoundException) {
}
return null
}
private fun getThemedCalendarIcon(
resources: Resources,
resId: Int,
iconProviderPackage: String
): LauncherIcon? {
try {
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
if (array.length() != 31) return null
return DynamicCalendarIcon(
resources = resources,
resourceIds = IntArray(31) {
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
},
isThemed = true
)
} catch (e: Resources.NotFoundException) {
}
return null
}
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.searchIconPackIcons("%$query%").map {
@ -244,6 +375,12 @@ class IconPackManager(
}
}
suspend fun searchThemedIcons(query: String): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.searchGreyscaleIcons("%$query%").map {
IconPackIcon(it)
}
}
}

View File

@ -8,6 +8,7 @@ import android.graphics.Color
import android.util.LruCache
import de.mm20.launcher2.customattrs.*
import de.mm20.launcher2.icons.providers.*
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
@ -66,7 +67,7 @@ class IconRepository(
val providers = mutableListOf<IconProvider>()
if (settings.themedIcons) {
providers.add(ThemedIconProvider(context))
providers.add(ThemedIconProvider(iconPackManager))
}
if (settings.iconPack.isNotBlank()) {
@ -87,6 +88,7 @@ class IconRepository(
val transformations = mutableListOf<LauncherIconTransformation>()
if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation())
if (settings.themedIcons && settings.forceThemed) transformations.add(ForceThemedIconTransformation())
this@IconRepository.placeholderProvider = placeholderProvider
iconProviders.value = providers
@ -140,6 +142,14 @@ class IconRepository(
)
)
}
if (customIcon is CustomThemedIcon) {
return listOf(
CustomThemedIconProvider(
customIcon,
iconPackManager
)
)
}
return emptyList()
}
@ -243,6 +253,15 @@ class IconRepository(
)
}
)
val themedIcon = iconPackManager.getGreyscaleIcon(searchable.`package`)
if (themedIcon != null && themedIcon.componentName?.packageName != null) {
providerOptions.add(
CustomThemedIcon(
iconPackageName = themedIcon.componentName.packageName,
)
)
}
}
suggestions.addAll(
@ -272,9 +291,9 @@ class IconRepository(
)
}
suspend fun searchIconPackIcon(query: String): List<CustomIconWithPreview> {
suspend fun searchCustomIcons(query: String): List<CustomIconWithPreview> {
val transformations = this.transformations.first()
return iconPackManager.searchIconPackIcon(query).mapNotNull {
val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview(
@ -285,6 +304,19 @@ class IconRepository(
preview = iconPackManager.getIcon(it.iconPack, componentName)?.transform(transformations) ?: return@mapNotNull null
)
}
val themedIcons = iconPackManager.searchThemedIcons(query).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview(
customIcon = CustomThemedIcon(
iconPackageName = componentName.packageName,
),
preview = iconPackManager.getThemedIcon(componentName.packageName)?.transform(transformations) ?: return@mapNotNull null
)
}
return iconPackIcons + themedIcons
}
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {

View File

@ -0,0 +1,15 @@
package de.mm20.launcher2.icons.providers
import de.mm20.launcher2.customattrs.CustomThemedIcon
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
class CustomThemedIconProvider(
private val customIcon: CustomThemedIcon,
private val iconPackManager: IconPackManager,
): IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
}
}

View File

@ -10,143 +10,16 @@ import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
internal class ThemedIconProvider(
private val context: Context,
private val iconPackManager: IconPackManager,
) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if (searchable !is Application) return null
val icon = getGreyscaleIcon(searchable.`package`) ?: return null
val resId = icon.drawable?.toIntOrNull() ?: return null
try {
val resources = context.packageManager.getResourcesForApplication(icon.iconPack)
return getClockIcon(resources, resId) ?: getCalendarIcon(
resources,
resId,
iconProviderPackage = icon.iconPack
) ?: getStaticIcon(resources, resId)
} catch (e: PackageManager.NameNotFoundException) {
CrashReporter.logException(e)
}
return null
}
private suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? {
val iconDao = AppDatabase.getInstance(context).iconDao()
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
?.let { IconPackIcon(it) }
}
private fun getStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
try {
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = fg,
scale = 0.5f,
),
backgroundLayer = ColorLayer()
)
} catch (e: Resources.NotFoundException) {
return null
}
}
private fun getClockIcon(resources: Resources, resId: Int): LauncherIcon? {
try {
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
var i = 0
var drawable: LayerDrawable? = null
var minuteIndex: Int? = null
var defaultMinute = 0
var hourIndex: Int? = null
var defaultHour = 0
while (i < array.length()) {
when (array.getString(i)) {
"com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> {
i++
drawable = array.getDrawable(i) as? LayerDrawable
}
"com.android.launcher3.HOUR_LAYER_INDEX" -> {
i++
hourIndex = array.getInt(i, -1).takeIf { it != -1 }
}
"com.android.launcher3.MINUTE_LAYER_INDEX" -> {
i++
minuteIndex = array.getInt(i, -1).takeIf { it != -1 }
}
"com.android.launcher3.DEFAULT_HOUR" -> {
i++
defaultHour = array.getInt(i, 0)
}
"com.android.launcher3.DEFAULT_MINUTE" -> {
i++
defaultMinute = array.getInt(i, 0)
}
}
i++
}
if (drawable != null && minuteIndex != null && hourIndex != null) {
return StaticLauncherIcon(
foregroundLayer = TintedClockLayer(
sublayers = (0 until drawable.numberOfLayers).map {
val drw = drawable.getDrawable(it)
if (drw is RotateDrawable) {
drw.level = when (it) {
hourIndex -> {
(12 - defaultHour) * 60
}
minuteIndex -> {
(60 - defaultMinute)
}
else -> 0
}
}
ClockSublayer(
drawable = drw,
role = when (it) {
hourIndex -> ClockSublayerRole.Hour
minuteIndex -> ClockSublayerRole.Minute
else -> ClockSublayerRole.Static
}
)
},
scale = 1.5f,
),
backgroundLayer = ColorLayer()
)
}
} catch (e: Resources.NotFoundException) {
}
return null
}
private fun getCalendarIcon(
resources: Resources,
resId: Int,
iconProviderPackage: String
): LauncherIcon? {
try {
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
if (array.length() != 31) return null
return DynamicCalendarIcon(
resources = resources,
resourceIds = IntArray(31) {
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
},
isThemed = true
)
} catch (e: Resources.NotFoundException) {
}
return null
return iconPackManager.getThemedIcon(searchable.`package`)
}
}

View File

@ -0,0 +1,32 @@
package de.mm20.launcher2.icons.transformations
import de.mm20.launcher2.icons.*
internal class ForceThemedIconTransformation : LauncherIconTransformation {
override suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = asThemed(icon.foregroundLayer),
backgroundLayer = ColorLayer(0),
)
}
private fun asThemed(layer: LauncherIconLayer): LauncherIconLayer {
return when(layer) {
is ClockLayer -> TintedClockLayer(
scale = layer.scale,
sublayers = layer.sublayers,
)
is ColorLayer -> layer.copy(color = 0)
is StaticIconLayer -> TintedIconLayer(
color = 0,
icon = layer.icon,
scale = layer.scale / 1.5f,
)
is TextLayer -> layer.copy(
color = 0
)
else -> layer
}
}
}

View File

@ -219,7 +219,9 @@ message Settings {
IconShape shape = 1;
bool themed_icons = 2;
string icon_pack = 3;
reserved 4;
bool adaptify = 5;
bool force_themed = 6;
}
IconSettings icons = 21;

View File

@ -60,7 +60,7 @@ class CustomizeSearchableSheetVM(
debounceSearchJob = launch {
delay(1000)
isSearchingIcons.value = true
iconSearchResults.value = iconRepository.searchIconPackIcon(query)
iconSearchResults.value = iconRepository.searchCustomIcons(query)
isSearchingIcons.value = false
}
}

View File

@ -199,6 +199,16 @@ fun AppearanceSettingsScreen() {
viewModel.setThemedIcons(it)
}
)
val forceThemedIcons by viewModel.forceThemedIcons.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_force_themed_icons),
summary = stringResource(R.string.preference_force_themed_icons_summary),
value = forceThemedIcons == true,
enabled = themedIcons == true,
onValueChanged = {
viewModel.setForceThemedIcons(it)
}
)
val iconPack by viewModel.iconPack.observeAsState()
val installedIconPacks by viewModel.installedIconPacks.observeAsState(emptyList())

View File

@ -165,6 +165,20 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
}
}
val forceThemedIcons = dataStore.data.map { it.icons.forceThemed }.asLiveData()
fun setForceThemedIcons(forceThemedIcons: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setForceThemed(forceThemedIcons)
)
.build()
}
}
}
val installedIconPacks: LiveData<List<IconPack>> = liveData {
emit(
listOf(