Initial changes

This commit is contained in:
SuperDragonXD
2025-11-08 22:42:04 +08:00
committed by Pun Butrach
parent 6bd7c85ab8
commit ba62bdf380
30 changed files with 460 additions and 287 deletions
+18
View File
@@ -0,0 +1,18 @@
**I. Core Architectural Principles (The "Lawnchair Way")**
* **Pragmatic Modernization:** The primary architectural goal is to incrementally refactor the
legacy AOSP codebase towards a modern, testable, and maintainable structure. We do not rewrite for
the sake of rewriting.
* **Decoupling & Abstraction:** New features and refactors must prioritize decoupling from the core
`Launcher3` (`src`) module wherever possible. The preferred pattern is to create clean Kotlin
interfaces (`lawnchair` package) that act as a bridge to legacy systems.
* **Unidirectional Data Flow (UDF) for UI:** All new, complex UI screens (especially in Settings)
are to be built using a ViewModel-centric UDF architecture. State should be exposed via
`StateFlow` and consumed by "dumb" Jetpack Compose UI.
* **Embrace Kotlin & Coroutines:** All new asynchronous code must use Kotlin Coroutines, preferably
`Flow`. Legacy callbacks and `AsyncTask`-style patterns are to be eliminated during refactoring.
* **Respect for Constraints:** The architecture must work within the project's real-world
constraints:
* **DI is a Service Locator:** We use a `MainThreadInitializedObject` singleton pattern, not
Hilt/Dagger. Note that this pattern is slowly being migrated to use Hilt/Dagger (without the
Android Gradle Plugin) by Google, so expect inconsistency.
@@ -201,7 +201,6 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
// Sometimes the user has to click the input bar one more time
// for the keyboard to show.
input.showKeyboard()
} else {
setBackgroundVisibility(true, 1f)
animateHintVisibility(false)
@@ -3,7 +3,7 @@ package app.lawnchair.data.iconoverride
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.lawnchair.icons.IconPickerItem
import app.lawnchair.icons.picker.IconPickerItem
import com.android.launcher3.util.ComponentKey
@Entity
@@ -2,7 +2,7 @@ package app.lawnchair.data.iconoverride
import android.content.Context
import app.lawnchair.data.AppDatabase
import app.lawnchair.icons.IconPickerItem
import app.lawnchair.icons.picker.IconPickerItem
import com.android.launcher3.LauncherAppState
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
@@ -1,8 +0,0 @@
package app.lawnchair.icons
import android.graphics.drawable.Drawable
data class IconEntryWithDrawable(
val entry: IconEntry,
val drawable: Drawable,
)
@@ -14,95 +14,80 @@ import android.content.Intent.ACTION_TIME_CHANGED
import android.content.Intent.ACTION_TIME_TICK
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageItemInfo
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.UserHandle
import android.os.UserManager
import android.text.TextUtils
import android.util.ArrayMap
import android.util.Log
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toDrawable
import app.lawnchair.data.iconoverride.IconOverrideRepository
import app.lawnchair.icons.iconpack.IconPack
import app.lawnchair.icons.iconpack.IconPackProvider
import app.lawnchair.icons.picker.IconEntry
import app.lawnchair.icons.picker.IconType
import app.lawnchair.preferences.PreferenceManager
import app.lawnchair.util.Constants.LAWNICONS_PACKAGE_NAME
import app.lawnchair.util.MultiSafeCloseable
import app.lawnchair.util.getPackageVersionCode
import app.lawnchair.util.isPackageInstalled
import com.android.launcher3.R
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.icons.IconProvider
import com.android.launcher3.icons.LauncherIconProvider.ATTR_DRAWABLE
import com.android.launcher3.icons.LauncherIconProvider.ATTR_PACKAGE
import com.android.launcher3.icons.LauncherIconProvider.TAG_ICON
import com.android.launcher3.icons.ClockDrawableWrapper
import com.android.launcher3.icons.LauncherIconProvider
import com.android.launcher3.icons.mono.ThemedIconDrawable
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.SafeCloseable
import javax.inject.Inject
import org.xmlpull.v1.XmlPullParser
class LawnchairIconProvider @JvmOverloads constructor(
private val context: Context,
supportsIconTheme: Boolean = false,
) : IconProvider(context) {
@LauncherAppSingleton
class LawnchairIconProvider @Inject constructor(
@ApplicationContext private val context: Context,
val themeManager: ThemeManager,
) : LauncherIconProvider(
context,
themeManager,
) {
private val prefs = PreferenceManager.getInstance(context)
private val themedIconsEnabled = prefs.themedIcons.get()
private val iconPackPref = prefs.iconPackPackage
private val themedIconPackPref = prefs.themedIconPackPackage
private val themedIconSourcePref = prefs.themedIconPackPackage
private val iconPackProvider = IconPackProvider.INSTANCE.get(context)
private val overrideRepo = IconOverrideRepository.INSTANCE.get(context)
private val iconPack
get() = iconPackProvider.getIconPack(iconPackPref.get())?.apply { loadBlocking() }
private val themedIconPack
get() = iconPackProvider.getIconPack(themedIconPackPref.get())?.apply { loadBlocking() }
private var isOlderLawniconsInstalled = context.packageManager.getPackageVersionCode(LAWNICONS_PACKAGE_NAME) in 1..3
private var iconPackVersion = 0L
private val themedIconSource
get() = iconPackProvider.getIconPack(themedIconSourcePref.get())?.apply { loadBlocking() }
private var themeMapName: String = ""
private var mThemedIconMap: Map<String, ThemeData>? = null
private val mThemeManager: ThemeManager? = null
private var _themeMap: Map<String, ThemeData>? = null
val themeMap: Map<String, ThemeData>
get() {
if (!context.isThemedIconsEnabled()) {
mThemedIconMap = DISABLED_MAP
if (!themedIconsEnabled) {
_themeMap = DISABLED_MAP
}
if (mThemedIconMap == null) {
mThemedIconMap = createThemedIconMap()
if (_themeMap == null) {
_themeMap = getThemedIconMap()
}
if (isOlderLawniconsInstalled) {
themeMapName = themedIconPackPref.get()
mThemedIconMap = createThemedIconMap()
if (themedIconSource != null && themeMapName == "") {
_themeMap = super.getThemedIconMap()
}
if (themedIconPack != null && themeMapName != themedIconPack!!.packPackageName) {
themeMapName = themedIconPack!!.packPackageName
mThemedIconMap = createThemedIconMap()
if (themedIconSource != null && themeMapName != themedIconSource!!.packPackageName) {
themeMapName = themedIconSource!!.packPackageName
_themeMap = getThemedIconMap()
}
return mThemedIconMap!!
return _themeMap!!
}
private val supportsIconTheme get() = themeMap != DISABLED_MAP
init {
setIconThemeSupported(supportsIconTheme)
}
/**
* Enables or disables icon theme support (Lawnchair)
* @see com.android.launcher3.icons.LauncherIconProvider.setIconThemeSupported
*/
fun setIconThemeSupported(isSupported: Boolean) {
if (isSupported && isOlderLawniconsInstalled && FeatureFlags.USE_LOCAL_ICON_OVERRIDES.get()) {
mThemedIconMap = null
} else {
mThemedIconMap = DISABLED_MAP
}
}
val systemIconState = themeManager.iconState
private fun resolveIconEntry(componentName: ComponentName, user: UserHandle): IconEntry? {
val componentKey = ComponentKey(componentName, user)
@@ -112,7 +97,7 @@ class LawnchairIconProvider @JvmOverloads constructor(
return overrideItem.toIconEntry()
}
val iconPack = this.iconPack ?: return null
val iconPack = iconPack ?: return null
// then look for dynamic calendar
val calendarEntry = iconPack.getCalendar(componentName)
if (calendarEntry != null) {
@@ -122,79 +107,136 @@ class LawnchairIconProvider @JvmOverloads constructor(
return iconPack.getIcon(componentName)
}
fun isThemeEnabled(): Boolean {
return mThemedIconMap != DISABLED_MAP
}
override fun getIcon(
info: PackageItemInfo,
appInfo: ApplicationInfo,
iconDpi: Int,
): Drawable {
val packageName = appInfo.packageName
val componentName = context.packageManager.getLaunchIntentForPackage(packageName)?.component
val user = UserHandle.getUserHandleForUid(appInfo.uid)
override fun getThemeDataForPackage(packageName: String?): ThemeData? {
return getThemedIconMap().get(packageName)
}
fun getThemedIconMap(): MutableMap<String, ThemeData> {
if (mThemedIconMap != null) {
return mThemedIconMap!!.toMutableMap() // Lawnchair-TODO: This feels cursed?
var iconEntry: IconEntry? = null
if (componentName != null) {
iconEntry = resolveIconEntry(componentName, user)
}
val map = ArrayMap<String, ThemeData>()
val res = mContext.getResources()
try {
res.getXml(R.xml.grayscale_icon_map).use { parser ->
val depth = parser.getDepth()
var type: Int
while ((parser.next().also { type = it }) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT
);
while ((
(parser.next().also { type = it }) != XmlPullParser.END_TAG ||
parser.getDepth() > depth
) &&
type != XmlPullParser.END_DOCUMENT
) {
if (type != XmlPullParser.START_TAG) {
continue
}
if (TAG_ICON == parser.getName()) {
val pkg = parser.getAttributeValue(null, ATTR_PACKAGE)
val iconId = parser.getAttributeResourceValue(
null,
ATTR_DRAWABLE,
0,
var iconPackEntry = iconEntry
val themeData = getThemeDataForPackage(packageName)
var themedIcon: Drawable? = null
val themedColors = ThemedIconDrawable.getColors(context)
if (iconEntry != null) {
val clock = iconPackProvider.getClockMetadata(iconEntry)
if (iconEntry.type == IconType.Calendar) {
iconPackEntry = iconEntry.resolveDynamicCalendar(getDay())
}
when {
!themedIconsEnabled -> {
// theming is disabled, don't populate theme data
themedIcon = null
}
clock != null -> {
// the icon supports dynamic clock, use dynamic themed clock
themedIcon =
ClockDrawableWrapper.forPackage(mContext, mClock.packageName, iconDpi)
.getMonochrome()
}
packageName == mClock.packageName -> {
// is clock app but icon might not be adaptive, fallback to static themed clock
val clockThemedData =
ThemeData(context.resources, R.drawable.themed_icon_static_clock)
themedIcon = CustomAdaptiveIconDrawable(
themedColors[0].toDrawable(),
clockThemedData.loadPaddedDrawable().apply { setTint(themedColors[1]) },
)
}
packageName == mCalendar.packageName -> {
// calendar app, apply the dynamic calendar icon
themedIcon = loadCalendarDrawable(iconDpi, themeData)
}
else -> {
// regular icon
themedIcon = if (themeData != null) {
CustomAdaptiveIconDrawable(
themedColors[0].toDrawable(),
themeData.loadPaddedDrawable().apply { setTint(themedColors[1]) },
)
if (iconId != 0 && !TextUtils.isEmpty(pkg)) {
map.put(pkg, ThemeData(res, iconId))
}
} else {
null
}
}
}
} catch (e: java.lang.Exception) {
Log.e(TAG, "Unable to parse icon map", e)
}
mThemedIconMap = map
return mThemedIconMap!!.toMutableMap() // Lawnchair-TODO: This feels cursed?
val iconPackIcon = iconPackEntry?.let { iconPackProvider.getDrawable(it, iconDpi, user) }
return themedIcon ?: iconPackIcon ?: super.getIcon(info, appInfo, iconDpi)
}
override fun getIcon(info: ComponentInfo?): Drawable {
return CustomAdaptiveIconDrawable.wrapNonNull(super.getIcon(info))
override fun getThemeDataForPackage(packageName: String?): ThemeData? {
return themeMap[packageName]
}
override fun getIcon(info: ComponentInfo?, iconDpi: Int): Drawable {
return CustomAdaptiveIconDrawable.wrapNonNull(super.getIcon(info, iconDpi))
}
override fun getThemedIconMap(): MutableMap<String, ThemeData> {
val themedIconMap = ArrayMap<String, ThemeData>()
override fun getIcon(info: ApplicationInfo?): Drawable {
return CustomAdaptiveIconDrawable.wrapNonNull(super.getIcon(info))
}
fun ArrayMap<String, ThemeData>.updateFromResources(
resources: Resources,
packageName: String,
) {
try {
@SuppressLint("DiscouragedApi")
val xmlId = resources.getIdentifier("grayscale_icon_map", "xml", packageName)
if (xmlId != 0) {
val parser = resources.getXml(xmlId)
val depth = parser.depth
var type: Int
while (
(
parser.next()
.also { type = it } != XmlPullParser.END_TAG || parser.depth > depth
) &&
type != XmlPullParser.END_DOCUMENT
) {
if (type != XmlPullParser.START_TAG) continue
if (TAG_ICON == parser.name) {
val pkg = parser.getAttributeValue(null, ATTR_PACKAGE)
val iconId = parser.getAttributeResourceValue(null, ATTR_DRAWABLE, 0)
if (iconId != 0 && pkg.isNotEmpty()) {
this[pkg] = ThemeData(resources, iconId)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to parse icon map.", e)
}
}
override fun getIcon(info: ApplicationInfo?, iconDpi: Int): Drawable {
return CustomAdaptiveIconDrawable.wrapNonNull(super.getIcon(info, iconDpi))
}
// first, get Lawnchair's internal grayscale icon map
themedIconMap.updateFromResources(
resources = context.resources,
packageName = context.packageName,
)
override fun getIcon(info: PackageItemInfo?, appInfo: ApplicationInfo?, iconDpi: Int): Drawable {
return CustomAdaptiveIconDrawable.wrapNonNull(super.getIcon(info, appInfo, iconDpi))
}
if (context.packageManager.isPackageInstalled(packageName = themeMapName)) {
// get the grayscale icon map of the supported icon pack
themedIconMap.updateFromResources(
resources = context.packageManager.getResourcesForApplication(themeMapName),
packageName = themeMapName,
)
}
override fun updateSystemState() {
super.updateSystemState()
mSystemState += "," + mThemeManager?.iconState?.toUniqueId()
return themedIconMap
}
override fun registerIconChangeListener(
@@ -204,7 +246,7 @@ class LawnchairIconProvider @JvmOverloads constructor(
return MultiSafeCloseable().apply {
add(super.registerIconChangeListener(callback, handler))
add(IconPackChangeReceiver(context, handler, callback))
add(LawniconsChangeReceiver(context, handler, callback))
add(LawniconsChangeReceiver(context, handler))
}
}
@@ -219,12 +261,12 @@ class LawnchairIconProvider @JvmOverloads constructor(
field?.close()
field = value
}
private var iconState = mThemeManager?.iconState
private var iconState = themeManager.iconState
private val iconPackPref = PreferenceManager.getInstance(context).iconPackPackage
private val themedIconPackPref = PreferenceManager.getInstance(context).themedIconPackPackage
private val subscription = iconPackPref.subscribeChanges {
val newState = mThemeManager?.iconState
val newState = themeManager.iconState
if (iconState != newState) {
iconState = newState
updateSystemState()
@@ -232,7 +274,7 @@ class LawnchairIconProvider @JvmOverloads constructor(
}
}
private val themedIconSubscription = themedIconPackPref.subscribeChanges {
val newState = mThemeManager?.iconState
val newState = themeManager.iconState
if (iconState != newState) {
iconState = newState
updateSystemState()
@@ -307,7 +349,6 @@ class LawnchairIconProvider @JvmOverloads constructor(
private inner class LawniconsChangeReceiver(
private val context: Context,
handler: Handler,
private val callback: IconChangeListener,
) : BroadcastReceiver(),
SafeCloseable {
@@ -321,9 +362,6 @@ class LawnchairIconProvider @JvmOverloads constructor(
}
override fun onReceive(context: Context, intent: Intent) {
if (isThemeEnabled()) {
setIconThemeSupported(true)
}
updateSystemState()
}
@@ -332,56 +370,7 @@ class LawnchairIconProvider @JvmOverloads constructor(
}
}
private fun createThemedIconMap(): MutableMap<String, ThemeData> {
val map = ArrayMap<String, ThemeData>()
fun updateMapFromResources(resources: Resources, packageName: String) {
try {
@SuppressLint("DiscouragedApi")
val xmlId = resources.getIdentifier("grayscale_icon_map", "xml", packageName)
if (xmlId != 0) {
val parser = resources.getXml(xmlId)
val depth = parser.depth
var type: Int
while (
(parser.next().also { type = it } != XmlPullParser.END_TAG || parser.depth > depth) &&
type != XmlPullParser.END_DOCUMENT
) {
if (type != XmlPullParser.START_TAG) continue
if (TAG_ICON == parser.name) {
val pkg = parser.getAttributeValue(null, ATTR_PACKAGE)
val iconId = parser.getAttributeResourceValue(null, ATTR_DRAWABLE, 0)
if (iconId != 0 && !pkg.isNullOrEmpty()) {
map[pkg] = ThemeData(resources, iconId)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to parse icon map.", e)
}
}
updateMapFromResources(
resources = context.resources,
packageName = context.packageName,
)
if (context.packageManager.isPackageInstalled(packageName = themeMapName)) {
iconPackVersion = context.packageManager.getPackageVersionCode(themeMapName)
updateMapFromResources(
resources = context.packageManager.getResourcesForApplication(themeMapName),
packageName = themeMapName,
)
if (isOlderLawniconsInstalled) {
// updateMapWithDynamicIcons(context, map)
}
}
return map
}
companion object {
const val TAG = "LawnchairIconProvider"
val DISABLED_MAP = emptyMap<String, ThemeData>()
}
}
@@ -0,0 +1,74 @@
package app.lawnchair.icons
import android.content.Context
import android.util.Log
import app.lawnchair.icons.shape.IconShape
import app.lawnchair.icons.shape.PathShapeDelegate
import app.lawnchair.preferences2.PreferenceManager2
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.concurrent.annotations.Ui
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.util.LooperExecutor
import com.patrykmichalik.opto.core.firstBlocking
import javax.inject.Inject
@LauncherAppSingleton
class LawnchairThemeManager
@Inject
constructor(
@ApplicationContext private val context: Context,
@Ui private val uiExecutor: LooperExecutor,
private val prefs: LauncherPrefs,
private val iconControllerFactory: IconControllerFactory,
private val lifecycle: DaggerSingletonTracker,
private val prefs2: PreferenceManager2,
) : ThemeManager(
context,
uiExecutor,
prefs,
iconControllerFactory,
lifecycle,
) {
override var iconState = parseIconStateV2(null)
override fun verifyIconState() {
val newState = parseIconStateV2(iconState)
if (newState == iconState) return
iconState = newState
listeners.forEach { it.onThemeChanged() }
}
private fun parseIconStateV2(oldState: IconState?): IconState {
val currentShape: IconShape = try {
prefs2.iconShape.firstBlocking()
} catch (e: Exception) {
Log.d(TAG, "Error getting icon shape", e)
IconShape.Circle
}
val shapePath = currentShape.getMaskPath()
val shapeKey = currentShape.getHashString()
val iconShape =
if (oldState != null && oldState.iconMask == shapeKey) {
oldState.iconShape
} else {
PathShapeDelegate(shapePath)
}
return IconState(
iconMask = shapeKey,
folderRadius = 1f,
shapeRadius = 1f,
themeController = iconControllerFactory.createThemeController(),
iconShape = iconShape,
folderShape = iconShape,
)
}
}
private const val TAG = "LawnchairThemeManager"
@@ -0,0 +1,37 @@
package app.lawnchair.icons
import android.content.Context
import app.lawnchair.preferences2.PreferenceManager2
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.concurrent.annotations.Ui
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.util.LooperExecutor
import dagger.Module
import dagger.Provides
@Module
class ThemeManagerModule {
@Provides
@LauncherAppSingleton
fun provideThemeManager(
@ApplicationContext context: Context,
@Ui uiExecutor: LooperExecutor,
prefs: LauncherPrefs,
lifecycle: DaggerSingletonTracker,
iconControllerFactory: ThemeManager.IconControllerFactory,
prefs2: PreferenceManager2,
): ThemeManager {
return LawnchairThemeManager(
context = context,
uiExecutor = uiExecutor,
prefs = prefs,
lifecycle = lifecycle,
iconControllerFactory = iconControllerFactory,
prefs2 = prefs2,
)
}
}
@@ -1,4 +1,4 @@
package app.lawnchair.icons
package app.lawnchair.icons.iconpack
import android.annotation.SuppressLint
import android.content.ComponentName
@@ -9,6 +9,12 @@ import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.drawable.Drawable
import android.util.Xml
import app.lawnchair.icons.ClockMetadata
import app.lawnchair.icons.ExtendedBitmapDrawable
import app.lawnchair.icons.picker.IconEntry
import app.lawnchair.icons.picker.IconPickerCategory
import app.lawnchair.icons.picker.IconPickerItem
import app.lawnchair.icons.picker.IconType
import com.android.launcher3.R
import java.io.IOException
import kotlinx.coroutines.Dispatchers
@@ -1,8 +1,12 @@
package app.lawnchair.icons
package app.lawnchair.icons.iconpack
import android.content.ComponentName
import android.content.Context
import android.graphics.drawable.Drawable
import app.lawnchair.icons.ClockMetadata
import app.lawnchair.icons.picker.IconEntry
import app.lawnchair.icons.picker.IconPickerCategory
import app.lawnchair.icons.picker.IconPickerItem
import com.android.launcher3.compat.AlphabeticIndexCompat
import java.util.concurrent.Semaphore
import kotlinx.coroutines.CoroutineName
@@ -1,17 +1,19 @@
package app.lawnchair.icons
package app.lawnchair.icons.iconpack
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Process
import android.os.UserHandle
import app.lawnchair.icons.ClockMetadata
import app.lawnchair.icons.CustomAdaptiveIconDrawable
import app.lawnchair.icons.picker.IconEntry
import app.lawnchair.icons.shouldTransparentBGIcons
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.icons.ClockDrawableWrapper
import com.android.launcher3.icons.IconProvider
import com.android.launcher3.util.DaggerSingletonObject
import com.android.launcher3.util.SafeCloseable
import javax.inject.Inject
@@ -49,26 +51,17 @@ class IconPackProvider @Inject constructor(
fun getDrawable(iconEntry: IconEntry, iconDpi: Int, user: UserHandle): Drawable? {
val iconPack = getIconPackOrSystem(iconEntry.packPackageName) ?: return null
iconPack.loadBlocking()
val packageManager = context.packageManager
val drawable = iconPack.getIcon(iconEntry, iconDpi) ?: return null
val shouldTintBackgrounds = context.shouldTintIconPackBackgrounds()
val clockMetadata =
if (user == Process.myUserHandle()) iconPack.getClock(iconEntry) else null
try {
if (clockMetadata != null) {
val clockDrawable: ClockDrawableWrapper =
ClockDrawableWrapper.forMeta(Build.VERSION.SDK_INT, clockMetadata) {
if (shouldTintBackgrounds) {
wrapThemedData(
packageManager,
iconEntry,
drawable,
)
} else {
drawable
}
drawable
}
return if (shouldTintBackgrounds && context.shouldTransparentBGIcons()) {
return if (context.shouldTransparentBGIcons()) {
clockDrawable.foreground
} else {
CustomAdaptiveIconDrawable(
@@ -81,31 +74,6 @@ class IconPackProvider @Inject constructor(
// Ignore
}
if (shouldTintBackgrounds) {
return wrapThemedData(packageManager, iconEntry, drawable)
}
return drawable
}
private fun wrapThemedData(
packageManager: PackageManager,
iconEntry: IconEntry,
drawable: Drawable,
): Drawable {
val res = packageManager.getResourcesForApplication(iconEntry.packPackageName)
val resId = res.getIdentifier(iconEntry.name, "drawable", iconEntry.packPackageName)
val td = IconProvider.ThemeData(res, resId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
drawable is AdaptiveIconDrawable &&
drawable.monochrome == null
) {
return AdaptiveIconDrawable(
drawable.background,
drawable.foreground,
td.loadPaddedDrawable(),
)
}
return drawable
}
@@ -1,10 +1,15 @@
package app.lawnchair.icons
package app.lawnchair.icons.iconpack
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherApps
import android.graphics.drawable.Drawable
import android.os.Process
import app.lawnchair.icons.ClockMetadata
import app.lawnchair.icons.picker.IconEntry
import app.lawnchair.icons.picker.IconPickerCategory
import app.lawnchair.icons.picker.IconPickerItem
import app.lawnchair.icons.picker.IconType
import app.lawnchair.util.requireSystemService
import com.android.launcher3.R
import com.android.launcher3.pm.UserCache
@@ -1,4 +1,4 @@
package app.lawnchair.icons
package app.lawnchair.icons.picker
data class IconEntry(
val packPackageName: String,
@@ -1,4 +1,4 @@
package app.lawnchair.icons
package app.lawnchair.icons.picker
data class IconPickerCategory(
val title: String,
@@ -1,4 +1,4 @@
package app.lawnchair.icons
package app.lawnchair.icons.picker
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -83,6 +83,9 @@ open class IconShape(
private val tmpPoint = PointF()
open val windowTransitionRadius = 1f
/** The icon scale used by Launcher3 */
open val iconScale = 1f
open fun getMaskPath(): Path {
return Path().also { addToPath(it, 0f, 0f, 100f, 100f, 50f) }
}
@@ -594,6 +597,8 @@ open class IconShape(
path.addPath(tempPath)
}
override val iconScale = 72f / 83.4f
override fun toString(): String {
return "foursidedcookie"
}
@@ -635,6 +640,8 @@ open class IconShape(
path.addPath(tempPath)
}
override val iconScale = 72f / 80f
override fun toString(): String {
return "sevensidedcookie"
}
@@ -0,0 +1,78 @@
package app.lawnchair.icons.shape
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.util.Log
import android.view.View
import com.android.launcher3.graphics.ShapeDelegate
import com.android.launcher3.views.ClipPathView
/**
* A ShapeDelegate that is initialized directly with a Path object,
* bypassing SVG string conversion. The provided path is assumed to be
* defined within a [0, 0, 100, 100] viewport.
*/
data class PathShapeDelegate(private val basePath: Path) : ShapeDelegate {
private val tmpPath = Path()
private val tmpMatrix = Matrix()
override fun drawShape(
canvas: Canvas,
offsetX: Float,
offsetY: Float,
radius: Float,
paint: Paint,
) {
tmpPath.reset()
addToPath(tmpPath, offsetX, offsetY, radius, tmpMatrix)
canvas.drawPath(tmpPath, paint)
}
override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
addToPath(path, offsetX, offsetY, radius, Matrix())
}
private fun addToPath(
path: Path,
offsetX: Float,
offsetY: Float,
radius: Float,
matrix: Matrix,
) {
// The base path is 100x100. We need to scale it to fit the desired 2 * radius.
// The radius in ShapeDelegate is half the size of the icon.
val scale = radius / 50f
matrix.setScale(scale, scale)
matrix.postTranslate(offsetX, offsetY)
// Apply the transformation to our base path and add it to the destination path.
basePath.transform(matrix, path)
}
override fun <T> createRevealAnimator(
target: T,
startRect: Rect,
endRect: Rect,
endRadius: Float,
isReversed: Boolean,
): android.animation.ValueAnimator where T : View, T : ClipPathView {
// This is complex to implement correctly without a proper morph.
// For now, we can fall back to a simple Rect-based animation,
// which is what would happen if a proper morph isn't possible anyway.
// A more advanced implementation would use androidx.graphics.shapes.Morph.
Log.w(
"PathShapeDelegate",
"createRevealAnimator is not fully implemented for custom paths.",
)
return ShapeDelegate.RoundedSquare(0f).createRevealAnimator(
target,
startRect,
endRect,
endRadius,
isReversed,
)
}
}
@@ -29,6 +29,7 @@ import com.android.launcher3.InvariantDeviceProfile.INDEX_DEFAULT
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.model.DeviceGridState
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.DaggerSingletonObject
@@ -43,6 +44,7 @@ class PreferenceManager @Inject constructor(
SafeCloseable {
private val idp get() = InvariantDeviceProfile.INSTANCE.get(context)
private val mRecentsModel get() = RecentsModel.INSTANCE.get(context)
private val themeManager = ThemeManager.INSTANCE.get(context)
private val reloadIcons: () -> Unit = { mRecentsModel.onThemeChanged() }
private val reloadGrid: () -> Unit = { idp.onPreferencesChanged(context) }
@@ -125,6 +125,9 @@ class PreferenceManager2 @Inject constructor(
?: IconShapeManager.getSystemIconShape(context)
},
save = { it.toString() },
onSet = {
reloadHelper.reloadIcons()
},
)
val customIconShape = preference(
@@ -17,7 +17,6 @@
package app.lawnchair.preferences2
import android.content.Context
import androidx.annotation.Discouraged
import app.lawnchair.LawnchairLauncher
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
@@ -44,9 +43,8 @@ class ReloadHelper(private val context: Context) {
recreate()
}
@Discouraged("This literally reload the models like forceRefresh because the old code has been removed in L3")
fun reloadIcons() {
LauncherAppState.INSTANCE.get(context).model.forceReload()
LauncherAppState.INSTANCE.get(context).iconCache.clearMemoryCache()
}
fun reloadTaskbar() {
@@ -68,7 +68,6 @@ import app.lawnchair.ui.preferences.components.WallpaperPreview
import app.lawnchair.ui.preferences.components.WithWallpaper
import app.lawnchair.ui.preferences.components.controls.ListPreference
import app.lawnchair.ui.preferences.components.controls.ListPreferenceEntry
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
import app.lawnchair.ui.preferences.components.invariantDeviceProfile
import app.lawnchair.ui.preferences.components.layout.Chip
import app.lawnchair.ui.preferences.components.layout.NestedScrollStretch
@@ -203,14 +202,6 @@ fun IconPackPreferences(
adapter = iconPackAdapter,
false,
)
PreferenceGroup {
Item {
SwitchPreference(
adapter = prefs.tintIconPackBackgrounds.getAdapter(),
label = stringResource(id = R.string.themed_icon_pack_tint),
)
}
}
}
1 -> {
@@ -280,11 +271,7 @@ fun IconPackGrid(
val lazyListState = rememberLazyListState()
val padding = 12.dp
val iconPacksLocal = if (isThemedIconPack) {
themedIconPacks.filter { it.packageName != "" }
} else {
iconPacks
}
val iconPacksLocal = iconPacks
val selectedPack = adapter.state.value
LaunchedEffect(selectedPack) {
@@ -34,11 +34,11 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.lawnchair.icons.CustomIconPack
import app.lawnchair.icons.IconPack
import app.lawnchair.icons.IconPackProvider
import app.lawnchair.icons.IconPickerItem
import app.lawnchair.icons.filter
import app.lawnchair.icons.iconpack.CustomIconPack
import app.lawnchair.icons.iconpack.IconPack
import app.lawnchair.icons.iconpack.IconPackProvider
import app.lawnchair.icons.picker.IconPickerItem
import app.lawnchair.icons.picker.filter
import app.lawnchair.ui.OverflowMenu
import app.lawnchair.ui.preferences.components.layout.PreferenceGroupDescription
import app.lawnchair.ui.preferences.components.layout.PreferenceLazyColumn
@@ -12,7 +12,7 @@ import androidx.compose.ui.res.stringResource
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.lawnchair.data.iconoverride.IconOverrideRepository
import app.lawnchair.icons.IconPickerItem
import app.lawnchair.icons.picker.IconPickerItem
import app.lawnchair.ui.preferences.LocalNavController
import app.lawnchair.ui.preferences.LocalPreferenceInteractor
import app.lawnchair.ui.preferences.components.AppItem
@@ -19,13 +19,19 @@ package com.android.launcher3.dagger;
import com.android.quickstep.dagger.QuickstepBaseAppComponent;
import app.lawnchair.icons.ThemeManagerModule;
import dagger.Component;
/**
* Root component for Dagger injection for Launcher Quickstep.
*/
@LauncherAppSingleton
@Component(modules = LauncherAppModule.class)
@Component(
modules = {
LauncherAppModule.class,
ThemeManagerModule.class
}
)
public interface LauncherAppComponent extends QuickstepBaseAppComponent {
/** Builder for quickstep LauncherAppComponent. */
@Component.Builder
@@ -16,9 +16,9 @@
package com.android.launcher3
import android.content.Context
import app.lawnchair.icons.LawnchairIconProvider
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.icons.IconCache
import com.android.launcher3.icons.LauncherIconProvider
import com.android.launcher3.util.DaggerSingletonObject
import javax.inject.Inject
import javax.inject.Named
@@ -29,7 +29,7 @@ data class LauncherAppState
@Inject
constructor(
@ApplicationContext val context: Context,
val iconProvider: LauncherIconProvider,
val iconProvider: LawnchairIconProvider,
val iconCache: IconCache,
val model: LauncherModel,
val invariantDeviceProfile: InvariantDeviceProfile,
@@ -20,24 +20,6 @@ import android.content.Context;
import androidx.annotation.Nullable;
import app.lawnchair.DeviceProfileOverrides;
import app.lawnchair.HeadlessWidgetsManager;
import app.lawnchair.NotificationManager;
import app.lawnchair.data.folder.service.FolderService;
import app.lawnchair.data.iconoverride.IconOverrideRepository;
import app.lawnchair.data.wallpaper.service.WallpaperService;
import app.lawnchair.font.FontCache;
import app.lawnchair.font.FontManager;
import app.lawnchair.font.googlefonts.GoogleFontsListing;
import app.lawnchair.icons.IconPackProvider;
import app.lawnchair.icons.shape.IconShapeManager;
import app.lawnchair.preferences.PreferenceManager;
import app.lawnchair.preferences2.PreferenceManager2;
import app.lawnchair.smartspace.provider.SmartspaceProvider;
import app.lawnchair.theme.ThemeProvider;
import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceModelList;
import app.lawnchair.ui.preferences.data.liveinfo.LiveInformationManager;
import app.lawnchair.util.LawnchairWindowManagerProxy;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherPrefs;
@@ -73,10 +55,28 @@ import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import com.android.launcher3.widget.util.WidgetSizeHandler;
import dagger.BindsInstance;
import javax.inject.Named;
import app.lawnchair.DeviceProfileOverrides;
import app.lawnchair.HeadlessWidgetsManager;
import app.lawnchair.NotificationManager;
import app.lawnchair.data.folder.service.FolderService;
import app.lawnchair.data.iconoverride.IconOverrideRepository;
import app.lawnchair.data.wallpaper.service.WallpaperService;
import app.lawnchair.font.FontCache;
import app.lawnchair.font.FontManager;
import app.lawnchair.font.googlefonts.GoogleFontsListing;
import app.lawnchair.icons.iconpack.IconPackProvider;
import app.lawnchair.icons.shape.IconShapeManager;
import app.lawnchair.preferences.PreferenceManager;
import app.lawnchair.preferences2.PreferenceManager2;
import app.lawnchair.smartspace.provider.SmartspaceProvider;
import app.lawnchair.theme.ThemeProvider;
import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceModelList;
import app.lawnchair.ui.preferences.data.liveinfo.LiveInformationManager;
import app.lawnchair.util.LawnchairWindowManagerProxy;
import dagger.BindsInstance;
/**
* Launcher base component for Dagger injection.
*
@@ -42,7 +42,7 @@ import javax.inject.Inject
/** Centralized class for managing Launcher icon theming */
@LauncherAppSingleton
class ThemeManager
open class ThemeManager
@Inject
constructor(
@ApplicationContext private val context: Context,
@@ -53,8 +53,8 @@ constructor(
) {
/** Representation of the current icon state */
var iconState = parseIconState(null)
private set
open var iconState = parseIconState(null)
protected set
var isMonoThemeEnabled
set(value) = prefs.put(THEMED_ICONS, value)
@@ -72,7 +72,7 @@ constructor(
val folderShape
get() = iconState.folderShape
private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
protected val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
init {
val receiver = SimpleBroadcastReceiver(
@@ -93,7 +93,7 @@ constructor(
}
}
private fun verifyIconState() {
protected open fun verifyIconState() {
val newState = parseIconState(iconState)
if (newState == iconState) return
iconState = newState
@@ -105,7 +105,7 @@ constructor(
fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener)
private fun parseIconState(oldState: IconState?): IconState {
protected open fun parseIconState(oldState: IconState?): IconState {
val shapeModel =
prefs.get(PREF_ICON_SHAPE).let { shapeOverride ->
ShapesProvider.iconShapes.firstOrNull { it.key == shapeOverride }
@@ -22,7 +22,6 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.LooperExecutor.CALLER_ICON_CACHE;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import static java.util.stream.Collectors.groupingBy;
import android.content.ComponentName;
@@ -87,6 +86,8 @@ import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import app.lawnchair.icons.LawnchairIconProvider;
/**
* Cache of application icons. Icons can be made from any thread.
*/
@@ -113,16 +114,14 @@ public class IconCache extends BaseIconCache {
private final SparseArray<BitmapInfo> mWidgetCategoryBitmapInfos;
private int mPendingIconRequestCount = 0;
@Inject
public IconCache(
@ApplicationContext Context context,
InvariantDeviceProfile idp,
@Nullable @Named("ICONS_DB") String dbFileName,
UserCache userCache,
LauncherIconProvider iconProvider,
// TODO: Lawnchair stuff
// IconProvider iconProvider,
LawnchairIconProvider iconProvider,
InstallSessionHelper installSessionHelper,
LauncherIcons.IconPool iconPool,
InstantAppResolver instantAppResolver,
@@ -184,6 +183,7 @@ public class IconCache extends BaseIconCache {
return mIconPool.obtain();
}
/**
/**
* Updates the entries related to the given package in memory and persistent DB.
*/
@@ -46,10 +46,10 @@ public class LauncherIconProvider extends IconProvider {
public static final String ATTR_DRAWABLE = "drawable";
public static final String ATTR_COMPONENT = "component";
private static final String TAG = "LIconProvider";
private static final Map<String, ThemeData> DISABLED_MAP = Collections.emptyMap();
protected static final String TAG = "LIconProvider";
protected static final Map<String, ThemeData> DISABLED_MAP = Collections.emptyMap();
private Map<String, ThemeData> mThemedIconMap;
protected Map<String, ThemeData> mThemedIconMap;
protected final ThemeManager mThemeManager;
@@ -73,7 +73,7 @@ public class LauncherIconProvider extends IconProvider {
mSystemState += "," + mThemeManager.getIconState().toUniqueId();
}
private Map<String, ThemeData> getThemedIconMap() {
protected Map<String, ThemeData> getThemedIconMap() {
if (mThemedIconMap != null) {
return mThemedIconMap;
}
@@ -21,6 +21,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ArchiveCompatibilityParams
import app.lawnchair.icons.LawnchairIconProvider
import com.android.launcher3.BuildConfigs
import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
@@ -31,7 +32,6 @@ import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
import com.android.launcher3.icons.IconCache
import com.android.launcher3.icons.LauncherIconProvider
import com.android.launcher3.icons.LauncherIcons.IconPool
import com.android.launcher3.notification.NotificationListener
import com.android.launcher3.pm.InstallSessionHelper
@@ -57,7 +57,7 @@ constructor(
private val themeManager: ThemeManager,
private val userCache: UserCache,
private val settingsCache: SettingsCache,
private val iconProvider: LauncherIconProvider,
private val iconProvider: LawnchairIconProvider,
private val customWidgetManager: CustomWidgetManager,
private val installSessionHelper: InstallSessionHelper,
private val lifeCycle: DaggerSingletonTracker,