Themed icons

This commit is contained in:
MM20 2021-12-04 21:39:45 +01:00
parent be50fb2276
commit ccda48c559
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
18 changed files with 357 additions and 125 deletions

View File

@ -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 {

View File

@ -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"

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>

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>

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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"

View File

@ -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))

View File

@ -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()) }
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)