Themed icons

This commit is contained in:
MM20
2021-12-04 21:39:45 +01:00
parent be50fb2276
commit ccda48c559
18 changed files with 357 additions and 125 deletions
@@ -95,6 +95,11 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
true
}
findPreference<Preference>("themed_icons")?.setOnPreferenceChangeListener { _, _ ->
iconRepository.recreate()
true
}
val shapePreference = findPreference<Preference>("icon_shape")!!
shapePreference.summary = getShapeName()
shapePreference.setOnPreferenceClickListener {
@@ -22,6 +22,12 @@
app:summary="@string/preference_cards_summary"
app:title="@string/preference_cards" />
<PreferenceCategory app:title="@string/preference_category_icons">
<SwitchPreference
app:key="themed_icons"
android:defaultValue="false"
app:title="@string/preference_themed_icons"
app:summary="@string/preference_themed_icons_summary"
/>
<ListPreference
app:key="icon_pack"
app:persistent="false"
+2
View File
@@ -413,4 +413,6 @@
<item quantity="other">+%1$d laufende Termine aus vergangenen Tagen</item>
</plurals>
<string name="weather_wind">Wind:</string>
<string name="preference_themed_icons">Eingefärbte Symbole</string>
<string name="preference_themed_icons_summary">Symbole an das Farbschema der App anpassen</string>
</resources>
+2
View File
@@ -203,6 +203,8 @@
<string name="preference_automatic_location_disabled_summary">Not supported by this provider</string>
<string name="date_format_long_without_year">EE, MMMM d</string>
<string name="weather_wind_direction_speed">%1$s • %2$s</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_icon_pack">Icon pack</string>
<string name="preference_icon_pack_summary_empty">No icon packs installed</string>
<string name="preference_dynamic_icon_bg">Dynamic background</string>
@@ -10,24 +10,20 @@ import java.util.*
import java.util.concurrent.Executors
class CalendarDynamicLauncherIcon(
context: Context,
foreground: Drawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
badgeNumber: Float = 0f,
val packageName: String,
val drawableIds: IntArray,
autoGenerateBackgroundMode: Int
foreground: Drawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
val packageName: String,
val drawableIds: IntArray,
autoGenerateBackgroundMode: Int
) : DynamicLauncherIcon(
foreground,
background,
foregroundScale,
backgroundScale,
foreground,
background,
foregroundScale,
backgroundScale,
/** Not needed, we already have a background **/
autoGenerateBackgroundMode,
badgeNumber,
null
autoGenerateBackgroundMode
) {
var currentDay = 0
@@ -7,37 +7,33 @@ import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable
import android.os.Build
import androidx.annotation.RequiresApi
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.*
import kotlin.math.roundToInt
@RequiresApi(Build.VERSION_CODES.O)
class ClockDynamicLauncherIcon(
context: Context,
foreground: LayerDrawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
badgeNumber: Float,
val hourLayer: Int,
val minuteLayer: Int,
val secondLayer: Int
foreground: LayerDrawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
val hourLayer: Int,
val minuteLayer: Int,
val secondLayer: Int
) : DynamicLauncherIcon(
foreground,
background,
foregroundScale,
backgroundScale,
foreground,
background,
foregroundScale,
backgroundScale,
/** Not needed, we already have a background **/
LauncherIcon.BACKGROUND_WHITE,
badgeNumber,
null
LauncherIcon.BACKGROUND_WHITE
) {
init {
foreground.also {
it.setDrawable(secondLayer, ColorDrawable(0))
try {
it.setDrawable(secondLayer, ColorDrawable(0))
} catch (e: IndexOutOfBoundsException) {}
(it.getDrawable(hourLayer) as? RotateDrawable)?.fromDegrees = 0f
(it.getDrawable(hourLayer) as? RotateDrawable)?.toDegrees = 360f
(it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f
@@ -2,17 +2,14 @@ package de.mm20.launcher2.icons
import android.content.Context
import android.graphics.drawable.Drawable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class DynamicLauncherIcon(
foreground: Drawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
autoGenerateBackgroundMode: Int,
badgeNumber: Float,
badgeDrawable: Drawable?)
foreground: Drawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
autoGenerateBackgroundMode: Int
)
: LauncherIcon(
foreground,
background,
@@ -3,38 +3,30 @@ package de.mm20.launcher2.icons
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherActivityInfo
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.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
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.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.ktx.randomElementOrNull
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LauncherPreferences
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.InputStreamReader
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",
)
class IconPackManager(
val context: Context,
val dynamicIconController: DynamicIconController
val context: Context
) {
var selectedIconPack: String
get() {
@@ -54,52 +46,6 @@ class IconPackManager(
selectedIconPack = iconPack
}
private fun getCalendarIcon(
context: Context,
activity: LauncherActivityInfo
): CalendarDynamicLauncherIcon? {
val component = ComponentName(activity.applicationInfo.packageName, activity.name)
val pm = context.packageManager
val ai = try {
pm.getActivityInfo(component, PackageManager.GET_META_DATA)
} catch (e: PackageManager.NameNotFoundException) {
return null
}
val resources = pm.getResourcesForActivity(component)
var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0
if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons")
?: return null
if (arrayId == 0) return null
val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null
if (typedArray.length() != 31) {
typedArray.recycle()
return null
}
val drawableIds = IntArray(31)
for (i in 0 until 31) {
drawableIds[i] = typedArray.getResourceId(i, 0)
}
typedArray.recycle()
return CalendarDynamicLauncherIcon(
context = context,
background = ColorDrawable(0),
foreground = ColorDrawable(0),
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = component.packageName,
drawableIds = drawableIds,
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
)
}
private fun getScale(): Float {
return when (LauncherPreferences.instance.iconShape) {
IconShape.CIRCLE, IconShape.PLATFORM_DEFAULT -> 0.7f
else -> 0.8f
}
}
suspend fun getInstalledIconPacks(): List<IconPack> {
return withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).iconDao().getInstalledIconPacks().map {
@@ -108,11 +54,6 @@ class IconPackManager(
}
}
companion object {
const val GOOGLE_DESK_CLOCK_PACKAGE_NAME = "com.google.android.deskclock"
const val GOOGLE_CALENDAR_PACKAGE_NAME = "com.google.android.calendar"
}
@Synchronized
suspend fun updateIconPacks() {
withContext(Dispatchers.IO) {
@@ -143,6 +84,9 @@ class UpdateIconPacksWorker(val context: Context) {
continue
}
}
val supportedGrayscaleMapPackages = SUPPORTED_GRAYSCALE_MAP_PROVIDERS
supportedGrayscaleMapPackages.forEach { installGrayscaleIconMap(it) }
}
private fun loadInstalledPacks(context: Context): List<ResolveInfo> {
@@ -296,6 +240,46 @@ class UpdateIconPacksWorker(val context: Context) {
CrashReporter.logException(e)
}
}
private fun installGrayscaleIconMap(packageName: String) {
val iconDao = AppDatabase.getInstance(context).iconDao()
try {
val resources = context.packageManager.getResourcesForApplication(packageName)
val resId = resources.getIdentifier("grayscale_icon_map", "xml", packageName)
if (resId == 0) {
iconDao.deleteIcons(packageName)
return
}
val icons = mutableListOf<Icon>()
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")
for (i in 0 until parser.attributeCount) {
Log.d("MM20", "${parser.getAttributeName(0)}")
}
val componentName = ComponentName(pkg, pkg)
val icon = Icon(
drawable = drawable,
componentName = componentName,
iconPack = packageName,
type = "greyscale_icon"
)
icons.add(icon)
Log.d("MM20", "Installed icon ${icon.toString()}")
}
}
}
iconDao.installGrayscaleIconMap(packageName, icons.map { it.toDatabaseEntity() })
} catch (e: PackageManager.NameNotFoundException) {
iconDao.deleteIcons(packageName)
return
}
}
}
private const val PREFERENCE_NAME = "icon_pack"
@@ -86,13 +86,20 @@ class IconRepository(
}
fun recreate() {
placeholderProvider = PlaceholderIconProvider(context)
placeholderProvider = if (LauncherPreferences.instance.themedIcons) {
ThemedPlaceholderIconProvider(context)
} else {
PlaceholderIconProvider(context)
}
val providers = mutableListOf<IconProvider>()
if (LauncherPreferences.instance.themedIcons) {
providers.add(ThemedIconProvider(context))
}
if (iconPackManager.selectedIconPack.isNotBlank()) {
providers.add(IconPackIconProvider(context, iconPackManager.selectedIconPack))
}
providers.add(GoogleClockIconProvider(context))
providers.add(CalendarIconProvider(context))
providers.add(SystemIconProvider(context))
@@ -5,6 +5,6 @@ import org.koin.dsl.module
val iconsModule = module {
single { DynamicIconController(androidContext()) }
single { IconPackManager(androidContext(), get()) }
single { IconPackManager(androidContext()) }
single { IconRepository(androidContext(), get(), get()) }
}
@@ -0,0 +1,46 @@
package de.mm20.launcher2.icons
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import de.mm20.launcher2.ktx.getDrawableOrNull
import java.util.*
import java.util.concurrent.Executors
class ThemedCalendarDynamicLauncherIcon(
foregroundScale: Float,
val packageName: String,
val foregroundIds: IntArray,
val foregroundTint: Int,
background: Drawable,
) : DynamicLauncherIcon(
foreground = ColorDrawable(0),
background = background,
foregroundScale = foregroundScale,
backgroundScale = 1f,
/** Not needed, we already have a background **/
BACKGROUND_WHITE
) {
var currentDay = 0
override fun update(context: Context) {
val calendar = Calendar.getInstance()
val day = calendar[Calendar.DAY_OF_MONTH]
if (day == currentDay || foregroundIds.size < currentDay) return
val resources = try {
context.packageManager.getResourcesForApplication(packageName)
} catch (e: PackageManager.NameNotFoundException) {
return
}
Executors.newSingleThreadExecutor().execute {
val currentDayDrawable = resources.getDrawableOrNull(foregroundIds[day - 1])
?: return@execute
currentDayDrawable.setTint(foregroundTint)
foreground = currentDayDrawable
}
currentDay = day
}
}
@@ -37,9 +37,8 @@ class CalendarIconProvider(val context: Context): IconProvider {
}
typedArray.recycle()
return CalendarDynamicLauncherIcon(
context = context,
background = ColorDrawable(0),
foreground = ColorDrawable(0),
background = ColorDrawable(0),
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = component.packageName,
@@ -8,7 +8,6 @@ import android.graphics.drawable.LayerDrawable
import android.os.Build
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.ClockDynamicLauncherIcon
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
@@ -21,7 +20,7 @@ class GoogleClockIconProvider(val context: Context) : IconProvider {
val pm = context.packageManager
val appInfo = try {
pm.getApplicationInfo(
IconPackManager.GOOGLE_DESK_CLOCK_PACKAGE_NAME,
"com.google.android.deskclock",
PackageManager.GET_META_DATA
)
} catch (e: PackageManager.NameNotFoundException) {
@@ -44,12 +43,10 @@ class GoogleClockIconProvider(val context: Context) : IconProvider {
val secondLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX")
return ClockDynamicLauncherIcon(
context = context,
background = baseIcon.background,
backgroundScale = 1.5f,
foreground = foreground,
background = baseIcon.background,
foregroundScale = 1.5f,
badgeNumber = 0f,
backgroundScale = 1.5f,
hourLayer = hourLayer,
minuteLayer = minuteLayer,
secondLayer = secondLayer
@@ -194,9 +194,8 @@ class IconPackIconProvider(val context: Context, val iconPack: String): IconProv
id
}.toIntArray()
return CalendarDynamicLauncherIcon(
context = context,
background = ColorDrawable(0),
foreground = ColorDrawable(0),
background = ColorDrawable(0),
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = iconPack,
@@ -6,6 +6,6 @@ import de.mm20.launcher2.search.data.Searchable
class SystemIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
return searchable.loadIconAsync(context, size)
return searchable.loadIcon(context, size)
}
}
@@ -0,0 +1,150 @@
package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.util.TypedValue
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.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
class ThemedIconProvider(
private val context: Context
) : IconProvider {
private val fgColor: Int
private val bgColor: Int
init {
val theme = context.resources.newTheme()
theme.applyStyle(R.style.DefaultColors, true)
val typedValue = TypedValue()
val isDarkMode =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES != 0
val bgAttr =
if (isDarkMode) R.attr.colorOnPrimaryContainer else R.attr.colorPrimaryContainer
val fgAttr = if (isDarkMode) R.attr.colorOnSurfaceInverse else R.attr.colorOnSurfaceVariant
bgColor = theme.resolveAttribute(bgAttr, typedValue, true).let {
typedValue.data
}
fgColor = theme.resolveAttribute(fgAttr, typedValue, true).let {
typedValue.data
}
}
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): Icon? {
val iconDao = AppDatabase.getInstance(context).iconDao()
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
?.let { Icon(it) }
}
private fun getStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
try {
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
fg.setTint(fgColor)
return LauncherIcon(
foreground = fg,
foregroundScale = 0.5f,
background = ColorDrawable(bgColor)
)
} catch (e: Resources.NotFoundException) {
return null
}
}
private fun getClockIcon(resources: Resources, resId: Int): LauncherIcon? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return null
}
try {
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
var i = 0
var drawable: LayerDrawable? = null
var minuteIndex: Int? = null
var hourIndex: Int? = null
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 }
}
}
i++
}
if (drawable != null && minuteIndex != null && hourIndex != null) {
drawable.setTint(fgColor)
return ClockDynamicLauncherIcon(
foreground = drawable,
background = ColorDrawable(bgColor),
foregroundScale = 1.5f,
backgroundScale = 1f,
hourLayer = hourIndex,
minuteLayer = minuteIndex,
secondLayer = -1,
)
}
} 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 ThemedCalendarDynamicLauncherIcon(
background = ColorDrawable(bgColor),
packageName = iconProviderPackage,
foregroundIds = IntArray(31) {
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
},
foregroundTint = fgColor,
foregroundScale = 0.5f,
)
} catch (e: Resources.NotFoundException) {
}
return null
}
}
@@ -0,0 +1,45 @@
package de.mm20.launcher2.icons.providers
import android.content.Context
import android.content.res.Configuration
import android.util.TypedValue
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.R
import de.mm20.launcher2.search.data.Searchable
class ThemedPlaceholderIconProvider(
val context: Context
) : IconProvider {
private val fgColor: Int
private val bgColor: Int
init {
val theme = context.resources.newTheme()
theme.applyStyle(R.style.DefaultColors, true)
val typedValue = TypedValue()
val isDarkMode =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES != 0
val bgAttr =
if (isDarkMode) R.attr.colorOnPrimaryContainer else R.attr.colorPrimaryContainer
val fgAttr = if (isDarkMode) R.attr.colorOnSurfaceInverse else R.attr.colorOnSurfaceVariant
bgColor = theme.resolveAttribute(bgAttr, typedValue, true).let {
typedValue.data
}
fgColor = theme.resolveAttribute(fgAttr, typedValue, true).let {
typedValue.data
}
}
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
val icon = searchable.getPlaceholderIcon(context)
icon.foreground.setTint(fgColor)
icon.background?.setTint(bgColor)
return icon
}
}
@@ -80,6 +80,7 @@ class LauncherPreferences(val context: Application, version: Int = 3) {
var calendarMaxEvents by StringPreference("calendar_max_events", default = "10")
var themedIcons by BooleanPreference("themed_icons", default = false)
var legacyIconBg by StringPreference("legacy_icon_bg", default = "1")
var blurCards by BooleanPreference("blur_cards", default = false)
var searchStyle by EnumPreference("search_style", default = SearchStyles.NO_BG)