diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt index 74003595..f1bd6e8e 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt @@ -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() } } diff --git a/database/src/main/java/de/mm20/launcher2/database/IconDao.kt b/database/src/main/java/de/mm20/launcher2/database/IconDao.kt index 85435747..29a007d5 100644 --- a/database/src/main/java/de/mm20/launcher2/database/IconDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/IconDao.kt @@ -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 - @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 + @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 + @Query("DELETE FROM Icons WHERE iconPack = :iconPack") fun deleteIcons(iconPack: String) diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 80ff8736..4d8bc0c7 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -454,6 +454,8 @@ Follow system Themed icons Color icons with the application\'s color scheme + Force themed icons + Apply the application\'s color scheme to all icons, including unsupported ones (not recommended) Icon pack No icon packs installed System bars diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt index 2dc42df2..9e2d1f84 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt @@ -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 { val iconDao = appDatabase.iconDao() return iconDao.searchIconPackIcons("%$query%").map { @@ -244,6 +375,12 @@ class IconPackManager( } } + suspend fun searchThemedIcons(query: String): List { + val iconDao = appDatabase.iconDao() + return iconDao.searchGreyscaleIcons("%$query%").map { + IconPackIcon(it) + } + } } diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt index 6b52c5b4..b24bdd2b 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -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() if (settings.themedIcons) { - providers.add(ThemedIconProvider(context)) + providers.add(ThemedIconProvider(iconPackManager)) } if (settings.iconPack.isNotBlank()) { @@ -87,6 +88,7 @@ class IconRepository( val transformations = mutableListOf() 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 { + suspend fun searchCustomIcons(query: String): List { 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?) { diff --git a/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt b/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt new file mode 100644 index 00000000..54d82181 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomThemedIconProvider.kt @@ -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) + } +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt b/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt index 39fa9a45..5f0ef52c 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/providers/ThemedIconProvider.kt @@ -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`) } } \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt b/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt new file mode 100644 index 00000000..f4753d2c --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/transformations/ForceThemedIconTransformation.kt @@ -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 + } + } + +} \ No newline at end of file diff --git a/preferences/src/main/proto/settings.proto b/preferences/src/main/proto/settings.proto index 4c456c5f..12464537 100644 --- a/preferences/src/main/proto/settings.proto +++ b/preferences/src/main/proto/settings.proto @@ -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; diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt index 62793b0f..83e522d5 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt @@ -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 } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt index 4c0fbb21..b3567a02 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt @@ -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()) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt index 700a41d8..5f09aa20 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt @@ -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> = liveData { emit( listOf(