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

View File

@ -19,9 +19,12 @@ interface IconDao {
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')") @Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity> suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
@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> 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") @Query("DELETE FROM Icons WHERE iconPack = :iconPack")
fun deleteIcons(iconPack: String) fun deleteIcons(iconPack: String)

View File

@ -454,6 +454,8 @@
<string name="preference_theme_system">Follow system</string> <string name="preference_theme_system">Follow system</string>
<string name="preference_themed_icons">Themed icons</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_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">Icon pack</string>
<string name="preference_icon_pack_summary_empty">No icon packs installed</string> <string name="preference_icon_pack_summary_empty">No icon packs installed</string>
<string name="preference_category_system_bars">System bars</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.Resources
import android.content.res.XmlResourceParser import android.content.res.XmlResourceParser
import android.graphics.* import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.customattrs.CustomIconPackIcon import de.mm20.launcher2.customattrs.CustomIconPackIcon
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.ktx.randomElementOrNull import de.mm20.launcher2.ktx.randomElementOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -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> { suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
val iconDao = appDatabase.iconDao() val iconDao = appDatabase.iconDao()
return iconDao.searchIconPackIcons("%$query%").map { 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 android.util.LruCache
import de.mm20.launcher2.customattrs.* import de.mm20.launcher2.customattrs.*
import de.mm20.launcher2.icons.providers.* 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.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.transform import de.mm20.launcher2.icons.transformations.transform
@ -66,7 +67,7 @@ class IconRepository(
val providers = mutableListOf<IconProvider>() val providers = mutableListOf<IconProvider>()
if (settings.themedIcons) { if (settings.themedIcons) {
providers.add(ThemedIconProvider(context)) providers.add(ThemedIconProvider(iconPackManager))
} }
if (settings.iconPack.isNotBlank()) { if (settings.iconPack.isNotBlank()) {
@ -87,6 +88,7 @@ class IconRepository(
val transformations = mutableListOf<LauncherIconTransformation>() val transformations = mutableListOf<LauncherIconTransformation>()
if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation()) if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation())
if (settings.themedIcons && settings.forceThemed) transformations.add(ForceThemedIconTransformation())
this@IconRepository.placeholderProvider = placeholderProvider this@IconRepository.placeholderProvider = placeholderProvider
iconProviders.value = providers iconProviders.value = providers
@ -140,6 +142,14 @@ class IconRepository(
) )
) )
} }
if (customIcon is CustomThemedIcon) {
return listOf(
CustomThemedIconProvider(
customIcon,
iconPackManager
)
)
}
return emptyList() 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( 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() val transformations = this.transformations.first()
return iconPackManager.searchIconPackIcon(query).mapNotNull { val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview( CustomIconWithPreview(
@ -285,6 +304,19 @@ class IconRepository(
preview = iconPackManager.getIcon(it.iconPack, componentName)?.transform(transformations) ?: return@mapNotNull null 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?) { 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.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
internal class ThemedIconProvider( internal class ThemedIconProvider(
private val context: Context, private val iconPackManager: IconPackManager,
) : IconProvider { ) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? { override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if (searchable !is Application) return null if (searchable !is Application) return null
val icon = getGreyscaleIcon(searchable.`package`) ?: return null return iconPackManager.getThemedIcon(searchable.`package`)
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
} }
} }

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; IconShape shape = 1;
bool themed_icons = 2; bool themed_icons = 2;
string icon_pack = 3; string icon_pack = 3;
reserved 4;
bool adaptify = 5; bool adaptify = 5;
bool force_themed = 6;
} }
IconSettings icons = 21; IconSettings icons = 21;

View File

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

View File

@ -199,6 +199,16 @@ fun AppearanceSettingsScreen() {
viewModel.setThemedIcons(it) 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 iconPack by viewModel.iconPack.observeAsState()
val installedIconPacks by viewModel.installedIconPacks.observeAsState(emptyList()) 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 { val installedIconPacks: LiveData<List<IconPack>> = liveData {
emit( emit(
listOf( listOf(