Reorganize and group modules
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,57 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.icons"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.palette)
|
||||
|
||||
implementation(libs.materialcomponents.core)
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":core:database"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":data:applications"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
api(project(":data:customattrs"))
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="org.adw.ActivityStarter.THEMES" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="com.novalauncher.THEME" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
|
||||
internal class DynamicCalendarIcon(
|
||||
val resources: Resources,
|
||||
val resourceIds: IntArray,
|
||||
val isThemed: Boolean = false,
|
||||
private var transformations: List<LauncherIconTransformation> = emptyList(),
|
||||
) : DynamicLauncherIcon, TransformableDynamicLauncherIcon {
|
||||
|
||||
init {
|
||||
if (resourceIds.size < 31) throw IllegalArgumentException("DynamicCalendarIcon resourceIds must at least have 31 items")
|
||||
}
|
||||
|
||||
override suspend fun getIcon(time: Long): StaticLauncherIcon = withContext(Dispatchers.IO) {
|
||||
val day = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).dayOfMonth
|
||||
val resId = resourceIds[day - 1]
|
||||
|
||||
val drawable = try {
|
||||
ResourcesCompat.getDrawable(resources, resId, null)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
null
|
||||
} ?: return@withContext StaticLauncherIcon(
|
||||
foregroundLayer = TextLayer(day.toString()),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
|
||||
var icon = if (isThemed) {
|
||||
StaticLauncherIcon(
|
||||
foregroundLayer = TintedIconLayer(
|
||||
icon = drawable,
|
||||
scale = 0.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
} else if (drawable is AdaptiveIconDrawable) {
|
||||
return@withContext StaticLauncherIcon(
|
||||
foregroundLayer = drawable.foreground?.let {
|
||||
StaticIconLayer(
|
||||
icon = it,
|
||||
scale = 1.5f,
|
||||
)
|
||||
} ?: TransparentLayer,
|
||||
backgroundLayer = drawable.background?.let {
|
||||
StaticIconLayer(
|
||||
icon = it,
|
||||
scale = 1.5f,
|
||||
)
|
||||
} ?: TransparentLayer,
|
||||
)
|
||||
} else StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = drawable,
|
||||
scale = 1f,
|
||||
),
|
||||
backgroundLayer = TransparentLayer
|
||||
)
|
||||
|
||||
for (transformation in transformations) {
|
||||
icon = transformation.transform(icon)
|
||||
}
|
||||
return@withContext icon
|
||||
}
|
||||
|
||||
override fun setTransformations(transformations: List<LauncherIconTransformation>) {
|
||||
this.transformations = transformations
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import de.mm20.launcher2.database.entities.IconPackEntity
|
||||
|
||||
data class IconPack(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
val version: String,
|
||||
var scale: Float = 1f
|
||||
) {
|
||||
constructor(entity: IconPackEntity) : this(
|
||||
name = entity.name,
|
||||
packageName = entity.packageName,
|
||||
version = entity.packageName,
|
||||
scale = entity.scale
|
||||
)
|
||||
|
||||
fun toDatabaseEntity(): IconPackEntity {
|
||||
return IconPackEntity(
|
||||
name = name,
|
||||
scale = scale,
|
||||
version = version,
|
||||
packageName = packageName
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import android.content.ComponentName
|
||||
import de.mm20.launcher2.database.entities.IconEntity
|
||||
|
||||
data class IconPackIcon(
|
||||
val type: String,
|
||||
val componentName: ComponentName?,
|
||||
val drawable: String?,
|
||||
val iconPack: String,
|
||||
val scale: Float? = null
|
||||
) {
|
||||
constructor(entity: IconEntity) : this(
|
||||
type = entity.type,
|
||||
componentName = entity.componentName,
|
||||
drawable = entity.drawable,
|
||||
iconPack = entity.iconPack,
|
||||
scale = entity.scale
|
||||
)
|
||||
|
||||
fun toDatabaseEntity(): IconEntity {
|
||||
return IconEntity(
|
||||
type = type,
|
||||
componentName = componentName,
|
||||
drawable = drawable,
|
||||
iconPack = iconPack,
|
||||
scale = scale
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.*
|
||||
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 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",
|
||||
"de.kvaesitso.icons",
|
||||
)
|
||||
|
||||
|
||||
class IconPackManager(
|
||||
private val context: Context,
|
||||
private val appDatabase: AppDatabase,
|
||||
) {
|
||||
suspend fun getInstalledIconPacks(): List<IconPack> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
appDatabase.iconDao().getInstalledIconPacks().map {
|
||||
IconPack(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateIconPacks() {
|
||||
withContext(Dispatchers.IO) {
|
||||
UpdateIconPacksWorker(context).doWork()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getIcon(iconPack: String, componentName: ComponentName): LauncherIcon? {
|
||||
val res = try {
|
||||
context.packageManager.getResourcesForApplication(iconPack)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.e("MM20", "Icon pack package $iconPack not found!")
|
||||
return null
|
||||
}
|
||||
val iconDao = appDatabase.iconDao()
|
||||
val icon = iconDao.getIcon(componentName.flattenToString(), iconPack)
|
||||
?: return null
|
||||
|
||||
val drawableName = icon.drawable ?: return null
|
||||
|
||||
if (icon.type == "calendar") {
|
||||
return getIconPackCalendarIcon(context, iconPack, drawableName)
|
||||
}
|
||||
val resId = res.getIdentifier(drawableName, "drawable", iconPack).takeIf { it != 0 }
|
||||
?: return null
|
||||
val drawable = try {
|
||||
ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
return when (drawable) {
|
||||
is AdaptiveIconDrawable -> {
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = drawable.foreground?.let {
|
||||
StaticIconLayer(
|
||||
icon = it,
|
||||
scale = 1.5f,
|
||||
)
|
||||
} ?: TransparentLayer,
|
||||
backgroundLayer = drawable.background?.let {
|
||||
StaticIconLayer(
|
||||
icon = it,
|
||||
scale = 1.5f,
|
||||
)
|
||||
} ?: TransparentLayer,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = drawable,
|
||||
scale = 1f
|
||||
),
|
||||
backgroundLayer = TransparentLayer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun generateIcon(
|
||||
context: Context,
|
||||
iconPack: String,
|
||||
baseIcon: Drawable,
|
||||
size: Int
|
||||
): LauncherIcon? {
|
||||
val back = getIconBack(iconPack)
|
||||
val upon = getIconUpon(iconPack)
|
||||
val mask = getIconMask(iconPack)
|
||||
val scale = getPackScale(iconPack)
|
||||
|
||||
if (back == null && upon == null && mask == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint()
|
||||
paint.isAntiAlias = true
|
||||
paint.isFilterBitmap = true
|
||||
paint.isDither = true
|
||||
|
||||
|
||||
var inBounds: Rect
|
||||
var outBounds: Rect
|
||||
|
||||
val icon = baseIcon.toBitmap(width = size, height = size)
|
||||
|
||||
inBounds = Rect(0, 0, icon.width, icon.height)
|
||||
outBounds = Rect(
|
||||
(bitmap.width * (1 - scale) * 0.5).roundToInt(),
|
||||
(bitmap.height * (1 - scale) * 0.5).roundToInt(),
|
||||
(bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(),
|
||||
(bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt()
|
||||
)
|
||||
canvas.drawBitmap(icon, inBounds, outBounds, paint)
|
||||
|
||||
val pack = iconPack
|
||||
val pm = context.packageManager
|
||||
val res = try {
|
||||
pm.getResourcesForApplication(pack)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (mask != null) {
|
||||
res.getIdentifier(mask, "drawable", pack).takeIf { it != 0 }?.let {
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
|
||||
val maskDrawable = try {
|
||||
ResourcesCompat.getDrawable(res, it, null) ?: return null
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
val maskBmp = maskDrawable.toBitmap(size, size)
|
||||
inBounds = Rect(0, 0, maskBmp.width, maskBmp.height)
|
||||
outBounds = Rect(0, 0, bitmap.width, bitmap.height)
|
||||
canvas.drawBitmap(maskBmp, inBounds, outBounds, paint)
|
||||
}
|
||||
}
|
||||
if (upon != null) {
|
||||
res.getIdentifier(upon, "drawable", pack).takeIf { it != 0 }?.let {
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
|
||||
val maskDrawable = try {
|
||||
ResourcesCompat.getDrawable(res, it, null) ?: return null
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
val maskBmp = maskDrawable.toBitmap(size, size)
|
||||
inBounds = Rect(0, 0, maskBmp.width, maskBmp.height)
|
||||
outBounds = Rect(0, 0, bitmap.width, bitmap.height)
|
||||
canvas.drawBitmap(maskBmp, inBounds, outBounds, paint)
|
||||
}
|
||||
}
|
||||
if (back != null) {
|
||||
res.getIdentifier(back, "drawable", pack).takeIf { it != 0 }?.let {
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
|
||||
val maskDrawable = try {
|
||||
ResourcesCompat.getDrawable(res, it, null) ?: return null
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
val maskBmp = maskDrawable.toBitmap(size, size)
|
||||
inBounds = Rect(0, 0, maskBmp.width, maskBmp.height)
|
||||
outBounds = Rect(0, 0, bitmap.width, bitmap.height)
|
||||
canvas.drawBitmap(maskBmp, inBounds, outBounds, paint)
|
||||
}
|
||||
}
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = BitmapDrawable(context.resources, bitmap),
|
||||
scale = 1f,
|
||||
),
|
||||
backgroundLayer = TransparentLayer
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackIcon> {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
return iconDao.getIconsFromAllPacks(componentName.flattenToString())
|
||||
.map { IconPackIcon(it) }
|
||||
}
|
||||
|
||||
private suspend fun getIconBack(iconPack: String): String? {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
val iconbacks = iconDao.getIconBacks(iconPack)
|
||||
return iconbacks.randomElementOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getIconUpon(iconPack: String): String? {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
val iconupons = iconDao.getIconUpons(iconPack)
|
||||
return iconupons.randomElementOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getIconMask(iconPack: String): String? {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
val iconmasks = iconDao.getIconMasks(iconPack)
|
||||
return iconmasks.randomElementOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getPackScale(iconPack: String): Float {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
return iconDao.getScale(iconPack) ?: 1f
|
||||
}
|
||||
|
||||
private fun getIconPackCalendarIcon(
|
||||
context: Context,
|
||||
iconPack: String,
|
||||
baseIconName: String
|
||||
): DynamicCalendarIcon? {
|
||||
val resources = try {
|
||||
context.packageManager.getResourcesForApplication(iconPack)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
val drawableIds = (1..31).map {
|
||||
val drawableName = baseIconName + it
|
||||
val id = resources.getIdentifier(drawableName, "drawable", iconPack)
|
||||
if (id == 0) return null
|
||||
id
|
||||
}.toIntArray()
|
||||
return DynamicCalendarIcon(
|
||||
resources = resources,
|
||||
resourceIds = drawableIds
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getThemedIcon(packageName: String): LauncherIcon? {
|
||||
val icon = getGreyscaleIcon(packageName) ?: return null
|
||||
val resId = icon.drawable?.toIntOrNull() ?: return null
|
||||
try {
|
||||
val resources = context.packageManager.getResourcesForApplication(icon.iconPack)
|
||||
return getThemedClockIcon(resources, resId) ?: getThemedCalendarIcon(
|
||||
resources,
|
||||
resId,
|
||||
iconProviderPackage = icon.iconPack
|
||||
) ?: getThemedStaticIcon(resources, resId)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? {
|
||||
val iconDao = AppDatabase.getInstance(context).iconDao()
|
||||
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
|
||||
?.let { IconPackIcon(it) }
|
||||
|
||||
}
|
||||
|
||||
private fun getThemedStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
|
||||
try {
|
||||
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedIconLayer(
|
||||
icon = fg,
|
||||
scale = 0.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThemedClockIcon(resources: Resources, resId: Int): LauncherIcon? {
|
||||
try {
|
||||
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
|
||||
var i = 0
|
||||
var drawable: LayerDrawable? = null
|
||||
var minuteIndex: Int? = null
|
||||
var defaultMinute = 0
|
||||
var hourIndex: Int? = null
|
||||
var defaultHour = 0
|
||||
while (i < array.length()) {
|
||||
when (array.getString(i)) {
|
||||
"com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> {
|
||||
i++
|
||||
drawable = array.getDrawable(i) as? LayerDrawable
|
||||
}
|
||||
"com.android.launcher3.HOUR_LAYER_INDEX" -> {
|
||||
i++
|
||||
hourIndex = array.getInt(i, -1).takeIf { it != -1 }
|
||||
}
|
||||
"com.android.launcher3.MINUTE_LAYER_INDEX" -> {
|
||||
i++
|
||||
minuteIndex = array.getInt(i, -1).takeIf { it != -1 }
|
||||
}
|
||||
"com.android.launcher3.DEFAULT_HOUR" -> {
|
||||
i++
|
||||
defaultHour = array.getInt(i, 0)
|
||||
}
|
||||
"com.android.launcher3.DEFAULT_MINUTE" -> {
|
||||
i++
|
||||
defaultMinute = array.getInt(i, 0)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (drawable != null && minuteIndex != null && hourIndex != null) {
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedClockLayer(
|
||||
sublayers = (0 until drawable.numberOfLayers).map {
|
||||
val drw = drawable.getDrawable(it)
|
||||
if (drw is RotateDrawable) {
|
||||
drw.level = when (it) {
|
||||
hourIndex -> {
|
||||
(12 - defaultHour) * 60
|
||||
}
|
||||
minuteIndex -> {
|
||||
(60 - defaultMinute)
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
ClockSublayer(
|
||||
drawable = drw,
|
||||
role = when (it) {
|
||||
hourIndex -> ClockSublayerRole.Hour
|
||||
minuteIndex -> ClockSublayerRole.Minute
|
||||
else -> ClockSublayerRole.Static
|
||||
}
|
||||
)
|
||||
},
|
||||
scale = 1.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
}
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getThemedCalendarIcon(
|
||||
resources: Resources,
|
||||
resId: Int,
|
||||
iconProviderPackage: String
|
||||
): LauncherIcon? {
|
||||
try {
|
||||
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
|
||||
if (array.length() != 31) return null
|
||||
|
||||
return DynamicCalendarIcon(
|
||||
resources = resources,
|
||||
resourceIds = IntArray(31) {
|
||||
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
|
||||
},
|
||||
isThemed = true
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
return iconDao.searchIconPackIcons("%$query%").map {
|
||||
IconPackIcon(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchThemedIcons(query: String): List<IconPackIcon> {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
return iconDao.searchGreyscaleIcons("%$query%").map {
|
||||
IconPackIcon(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class UpdateIconPacksWorker(val context: Context) {
|
||||
|
||||
fun doWork() {
|
||||
val packs = loadInstalledPacks(context).map { it.activityInfo.packageName }
|
||||
val grayscaleProviders = loadInstalledGreyscaleProviders(context)
|
||||
val iconDao = AppDatabase.getInstance(context).iconDao()
|
||||
iconDao.uninstallIconPacksExcept(
|
||||
packs.union(grayscaleProviders).toList()
|
||||
)
|
||||
|
||||
for (pack in packs) {
|
||||
try {
|
||||
val packInfo = context.packageManager.getPackageInfo(pack, 0)
|
||||
val iconPack = IconPack(
|
||||
name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(),
|
||||
packageName = pack,
|
||||
version = packInfo.versionName
|
||||
)
|
||||
//if (iconDao.isInstalled(iconPack)) continue
|
||||
installIconPack(iconPack)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
val supportedGrayscaleMapPackages = SUPPORTED_GRAYSCALE_MAP_PROVIDERS
|
||||
supportedGrayscaleMapPackages.forEach { installGrayscaleIconMap(it) }
|
||||
}
|
||||
|
||||
private fun loadInstalledGreyscaleProviders(context: Context): List<String> {
|
||||
val pm = context.packageManager
|
||||
return SUPPORTED_GRAYSCALE_MAP_PROVIDERS.filter {
|
||||
try {
|
||||
pm.getPackageInfo(it, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInstalledPacks(context: Context): List<ResolveInfo> {
|
||||
val packs = mutableListOf<ResolveInfo>()
|
||||
val pm = context.packageManager
|
||||
var intent = Intent("org.adw.ActivityStarter.THEMES")
|
||||
val adwPacks = pm.queryIntentActivities(intent, 0)
|
||||
packs.addAll(adwPacks)
|
||||
intent = Intent("com.novalauncher.THEME")
|
||||
val novaPacks = pm.queryIntentActivities(intent, 0)
|
||||
novaPacks.forEach {
|
||||
if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
packs.sortWith(ResolveInfo.DisplayNameComparator(pm))
|
||||
return packs
|
||||
}
|
||||
|
||||
private fun installIconPack(iconPack: IconPack) {
|
||||
val pkgName = iconPack.packageName
|
||||
|
||||
val icons = mutableListOf<IconPackIcon>()
|
||||
val database = AppDatabase.getInstance(context)
|
||||
database.runInTransaction {
|
||||
try {
|
||||
val res = context.packageManager.getResourcesForApplication(pkgName)
|
||||
val parser: XmlPullParser
|
||||
var inStream: InputStreamReader? = null
|
||||
val xmlId = res.getIdentifier("appfilter", "xml", pkgName)
|
||||
if (xmlId != 0) parser = res.getXml(xmlId)
|
||||
else {
|
||||
val rawId = res.getIdentifier("appfilter", "raw", pkgName)
|
||||
if (rawId == 0) {
|
||||
Log.e(
|
||||
"MM20",
|
||||
"Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw"
|
||||
)
|
||||
return@runInTransaction
|
||||
}
|
||||
parser = XmlPullParserFactory.newInstance().newPullParser()
|
||||
inStream = res.openRawResource(rawId).reader()
|
||||
parser.setInput(inStream)
|
||||
}
|
||||
val iconDao = database.iconDao()
|
||||
|
||||
iconDao.deleteIconPack(iconPack.toDatabaseEntity())
|
||||
iconDao.deleteIcons(iconPack.packageName)
|
||||
|
||||
loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) continue
|
||||
when (parser.name) {
|
||||
"item" -> {
|
||||
val component = parser.getAttributeValue(null, "component")
|
||||
?: continue@loop
|
||||
val drawable = parser.getAttributeValue(null, "drawable")
|
||||
?: continue@loop
|
||||
if (component.length <= 14) continue@loop
|
||||
val componentName = ComponentName.unflattenFromString(
|
||||
component.substring(
|
||||
14,
|
||||
component.lastIndex
|
||||
)
|
||||
)
|
||||
?: continue@loop
|
||||
val icon = IconPackIcon(
|
||||
componentName = componentName,
|
||||
drawable = drawable,
|
||||
iconPack = pkgName,
|
||||
type = "app"
|
||||
)
|
||||
icons.add(icon)
|
||||
}
|
||||
"calendar" -> {
|
||||
val component = parser.getAttributeValue(null, "component")
|
||||
?: continue@loop
|
||||
val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop
|
||||
if (component.length < 14) continue@loop
|
||||
val componentName = ComponentName.unflattenFromString(
|
||||
component.substring(
|
||||
14,
|
||||
component.lastIndex
|
||||
)
|
||||
)
|
||||
?: continue@loop
|
||||
|
||||
val icon = IconPackIcon(
|
||||
componentName = componentName,
|
||||
drawable = drawable,
|
||||
iconPack = pkgName,
|
||||
type = "calendar"
|
||||
)
|
||||
icons.add(icon)
|
||||
}
|
||||
"iconback" -> {
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
if (parser.getAttributeName(i).startsWith("img")) {
|
||||
val drawable = parser.getAttributeValue(i)
|
||||
val icon = IconPackIcon(
|
||||
componentName = null,
|
||||
drawable = drawable,
|
||||
iconPack = pkgName,
|
||||
type = "iconback"
|
||||
)
|
||||
icons.add(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
"iconupon" -> {
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
if (parser.getAttributeName(i).startsWith("img")) {
|
||||
val drawable = parser.getAttributeValue(i)
|
||||
val icon = IconPackIcon(
|
||||
componentName = null,
|
||||
drawable = drawable,
|
||||
iconPack = pkgName,
|
||||
type = "iconupon"
|
||||
)
|
||||
icons.add(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
"iconmask" -> {
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
if (parser.getAttributeName(i).startsWith("img")) {
|
||||
val drawable = parser.getAttributeValue(i)
|
||||
val icon = IconPackIcon(
|
||||
componentName = null,
|
||||
drawable = drawable,
|
||||
iconPack = pkgName,
|
||||
type = "iconmask"
|
||||
)
|
||||
icons.add(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
"scale" -> {
|
||||
val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull()
|
||||
?: continue@loop
|
||||
iconPack.scale = scale
|
||||
}
|
||||
}
|
||||
if (icons.size >= 100) {
|
||||
iconDao.insertAll(icons.map { it.toDatabaseEntity() })
|
||||
icons.clear()
|
||||
}
|
||||
}
|
||||
|
||||
if (icons.isNotEmpty()) {
|
||||
iconDao.insertAll(icons.map { it.toDatabaseEntity() })
|
||||
}
|
||||
iconDao.installIconPack(iconPack.toDatabaseEntity())
|
||||
|
||||
(parser as? XmlResourceParser)?.close()
|
||||
inStream?.close()
|
||||
|
||||
Log.d("MM20", "Icon pack has been installed successfully")
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.e("MM20", "Could not install icon pack $pkgName: package not found.")
|
||||
} catch (e: XmlPullParserException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun installGrayscaleIconMap(packageName: String) {
|
||||
val database = AppDatabase.getInstance(context)
|
||||
database.runInTransaction {
|
||||
val iconDao = database.iconDao()
|
||||
try {
|
||||
val resources = context.packageManager.getResourcesForApplication(packageName)
|
||||
val resId = resources.getIdentifier("grayscale_icon_map", "xml", packageName)
|
||||
iconDao.deleteIcons(packageName)
|
||||
if (resId == 0) {
|
||||
return@runInTransaction
|
||||
}
|
||||
val icons = mutableListOf<IconPackIcon>()
|
||||
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")
|
||||
val componentName = ComponentName(pkg, pkg)
|
||||
val icon = IconPackIcon(
|
||||
drawable = drawable,
|
||||
componentName = componentName,
|
||||
iconPack = packageName,
|
||||
type = "greyscale_icon"
|
||||
)
|
||||
icons.add(icon)
|
||||
}
|
||||
}
|
||||
if (icons.size >= 100) {
|
||||
iconDao.insertAll(icons.map { it.toDatabaseEntity() })
|
||||
icons.clear()
|
||||
}
|
||||
}
|
||||
if (icons.isNotEmpty()) {
|
||||
iconDao.insertAll(icons.map { it.toDatabaseEntity() })
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
iconDao.deleteIcons(packageName)
|
||||
return@runInTransaction
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val PREFERENCE_NAME = "icon_pack"
|
||||
private const val KEY_ICON_PACK = "icon_pack"
|
||||
private const val KEY_VERSION = "version"
|
||||
private const val KEY_ICONSCALE = "iconscale"
|
||||
@@ -0,0 +1,388 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.util.LruCache
|
||||
import de.mm20.launcher2.data.customattrs.AdaptifiedLegacyIcon
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.data.customattrs.CustomIcon
|
||||
import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
|
||||
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
|
||||
import de.mm20.launcher2.data.customattrs.DefaultPlaceholderIcon
|
||||
import de.mm20.launcher2.data.customattrs.ForceThemedIcon
|
||||
import de.mm20.launcher2.data.customattrs.UnmodifiedSystemDefaultIcon
|
||||
import de.mm20.launcher2.icons.providers.CalendarIconProvider
|
||||
import de.mm20.launcher2.icons.providers.CustomIconPackIconProvider
|
||||
import de.mm20.launcher2.icons.providers.CustomThemedIconProvider
|
||||
import de.mm20.launcher2.icons.providers.GoogleClockIconProvider
|
||||
import de.mm20.launcher2.icons.providers.IconPackIconProvider
|
||||
import de.mm20.launcher2.icons.providers.IconProvider
|
||||
import de.mm20.launcher2.icons.providers.PlaceholderIconProvider
|
||||
import de.mm20.launcher2.icons.providers.SystemIconProvider
|
||||
import de.mm20.launcher2.icons.providers.ThemedIconProvider
|
||||
import de.mm20.launcher2.icons.providers.ThemedPlaceholderIconProvider
|
||||
import de.mm20.launcher2.icons.providers.getFirstIcon
|
||||
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
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IconRepository(
|
||||
val context: Context,
|
||||
private val iconPackManager: IconPackManager,
|
||||
private val dataStore: LauncherDataStore,
|
||||
private val customAttributesRepository: CustomAttributesRepository,
|
||||
) {
|
||||
|
||||
private val appReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
requestIconPackListUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
private val cache = LruCache<String, LauncherIcon>(200)
|
||||
|
||||
private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf())
|
||||
private var placeholderProvider: IconProvider? = null
|
||||
|
||||
private var transformations: MutableStateFlow<List<LauncherIconTransformation>> =
|
||||
MutableStateFlow(
|
||||
listOf()
|
||||
)
|
||||
|
||||
init {
|
||||
requestIconPackListUpdate()
|
||||
context.registerReceiver(appReceiver, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
|
||||
scope.launch {
|
||||
dataStore.data.map { it.icons }.distinctUntilChanged().collectLatest { settings ->
|
||||
val placeholderProvider = if (settings.themedIcons) {
|
||||
ThemedPlaceholderIconProvider(context)
|
||||
} else {
|
||||
PlaceholderIconProvider(context)
|
||||
}
|
||||
val providers = mutableListOf<IconProvider>()
|
||||
|
||||
if (settings.themedIcons) {
|
||||
providers.add(ThemedIconProvider(iconPackManager))
|
||||
}
|
||||
|
||||
if (settings.iconPack.isNotBlank()) {
|
||||
providers.add(
|
||||
IconPackIconProvider(
|
||||
context,
|
||||
settings.iconPack,
|
||||
iconPackManager
|
||||
)
|
||||
)
|
||||
}
|
||||
providers.add(GoogleClockIconProvider(context))
|
||||
providers.add(CalendarIconProvider(context))
|
||||
providers.add(SystemIconProvider(context, settings.themedIcons))
|
||||
providers.add(placeholderProvider)
|
||||
cache.evictAll()
|
||||
|
||||
val transformations = mutableListOf<LauncherIconTransformation>()
|
||||
|
||||
if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation())
|
||||
if (settings.themedIcons && settings.forceThemed) transformations.add(
|
||||
ForceThemedIconTransformation()
|
||||
)
|
||||
|
||||
this@IconRepository.placeholderProvider = placeholderProvider
|
||||
iconProviders.value = providers
|
||||
this@IconRepository.transformations.value = transformations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon> = channelFlow {
|
||||
iconProviders.collectLatest { providers ->
|
||||
transformations.collectLatest { transformations ->
|
||||
customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon ->
|
||||
|
||||
val provs = getProviders(customIcon) + providers
|
||||
val transforms = getTransformations(customIcon) ?: transformations
|
||||
|
||||
var icon = cache.get(searchable.key + customIcon.hashCode())
|
||||
if (icon != null) {
|
||||
send(icon)
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
val placeholder = placeholderProvider?.getIcon(searchable, size)
|
||||
placeholder?.let { send(it) }
|
||||
|
||||
icon = provs.getFirstIcon(searchable, size)
|
||||
|
||||
if (icon != null) {
|
||||
icon = icon.transform(transforms)
|
||||
|
||||
cache.put(searchable.key + customIcon.hashCode(), icon)
|
||||
send(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProviders(customIcon: CustomIcon?): List<IconProvider> {
|
||||
if (customIcon is UnmodifiedSystemDefaultIcon) {
|
||||
return listOf(
|
||||
SystemIconProvider(context, false)
|
||||
)
|
||||
}
|
||||
if (customIcon is CustomIconPackIcon) {
|
||||
return listOf(
|
||||
CustomIconPackIconProvider(
|
||||
customIcon,
|
||||
iconPackManager
|
||||
)
|
||||
)
|
||||
}
|
||||
if (customIcon is CustomThemedIcon) {
|
||||
return listOf(
|
||||
CustomThemedIconProvider(
|
||||
customIcon,
|
||||
iconPackManager
|
||||
)
|
||||
)
|
||||
}
|
||||
if (customIcon is DefaultPlaceholderIcon) {
|
||||
return placeholderProvider?.let { listOf(it) } ?: emptyList()
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun getTransformations(customIcon: CustomIcon?): List<LauncherIconTransformation>? {
|
||||
customIcon ?: return null
|
||||
if (customIcon is AdaptifiedLegacyIcon) {
|
||||
return listOf(
|
||||
LegacyToAdaptiveTransformation(
|
||||
foregroundScale = customIcon.fgScale,
|
||||
backgroundColor = customIcon.bgColor
|
||||
)
|
||||
)
|
||||
}
|
||||
if (customIcon is ForceThemedIcon) {
|
||||
return listOf(
|
||||
ForceThemedIconTransformation()
|
||||
)
|
||||
}
|
||||
if (customIcon is UnmodifiedSystemDefaultIcon) {
|
||||
return emptyList()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun requestIconPackListUpdate() {
|
||||
scope.launch {
|
||||
iconPackManager.updateIconPacks()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getInstalledIconPacks(): List<IconPack> {
|
||||
return iconPackManager.getInstalledIconPacks()
|
||||
}
|
||||
|
||||
suspend fun getCustomIconSuggestions(
|
||||
searchable: SavableSearchable,
|
||||
size: Int
|
||||
): List<CustomIconWithPreview> {
|
||||
val suggestions = mutableListOf<CustomIconWithPreview>()
|
||||
|
||||
val rawIcon = iconProviders.first().getFirstIcon(searchable, size) ?: return emptyList()
|
||||
|
||||
val defaultTransformations = transformations.first()
|
||||
|
||||
val transformationOptions = mutableListOf<CustomIcon>(UnmodifiedSystemDefaultIcon)
|
||||
|
||||
if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) {
|
||||
// Legacy icons that simply fill the entire canvas
|
||||
transformationOptions.add(
|
||||
AdaptifiedLegacyIcon(
|
||||
fgScale = 1f,
|
||||
bgColor = 1
|
||||
)
|
||||
)
|
||||
// 48x48 with 5px padding used to be the default icon size for icons generated by
|
||||
// the Android Studio asset generator. Upscale these icons to remove that padding.
|
||||
|
||||
transformationOptions.add(
|
||||
AdaptifiedLegacyIcon(
|
||||
fgScale = 48f / 38f,
|
||||
bgColor = 1
|
||||
)
|
||||
)
|
||||
|
||||
// Android 7.1 round icons (48x48 circle with 1px padding)
|
||||
transformationOptions.add(
|
||||
AdaptifiedLegacyIcon(
|
||||
fgScale = 48f / 44f,
|
||||
bgColor = 1
|
||||
)
|
||||
)
|
||||
transformationOptions.add(
|
||||
AdaptifiedLegacyIcon(
|
||||
fgScale = 0.7f,
|
||||
bgColor = 0
|
||||
)
|
||||
)
|
||||
transformationOptions.add(
|
||||
AdaptifiedLegacyIcon(
|
||||
fgScale = 0.7f,
|
||||
bgColor = Color.WHITE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val providerOptions = mutableListOf<CustomIcon>()
|
||||
|
||||
if (searchable is LauncherApp) {
|
||||
val iconPackIcons = iconPackManager.getAllIconPackIcons(
|
||||
searchable.launcherActivityInfo.componentName
|
||||
)
|
||||
|
||||
providerOptions.addAll(
|
||||
iconPackIcons.mapNotNull {
|
||||
CustomIconPackIcon(
|
||||
iconPackPackage = it.iconPack,
|
||||
iconComponentName = it.componentName?.flattenToString()
|
||||
?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val themedIcon = iconPackManager.getGreyscaleIcon(searchable.`package`)
|
||||
if (themedIcon != null && themedIcon.componentName?.packageName != null) {
|
||||
providerOptions.add(
|
||||
CustomThemedIcon(
|
||||
iconPackageName = themedIcon.componentName.packageName,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
transformationOptions.add(
|
||||
ForceThemedIcon
|
||||
)
|
||||
}
|
||||
} else {
|
||||
transformationOptions.add(
|
||||
ForceThemedIcon
|
||||
)
|
||||
}
|
||||
|
||||
providerOptions.add(DefaultPlaceholderIcon)
|
||||
|
||||
suggestions.addAll(
|
||||
transformationOptions.map {
|
||||
val transformations = getTransformations(it) ?: defaultTransformations
|
||||
val providers = getProviders(it)
|
||||
|
||||
val icon = providers.getFirstIcon(searchable, size) ?: rawIcon
|
||||
|
||||
CustomIconWithPreview(
|
||||
preview = icon.transform(transformations),
|
||||
customIcon = it,
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
suggestions.addAll(
|
||||
providerOptions.mapNotNull {
|
||||
val providers = getProviders(it)
|
||||
|
||||
val icon = providers.getFirstIcon(searchable, size) ?: return@mapNotNull null
|
||||
|
||||
CustomIconWithPreview(
|
||||
preview = icon.transform(defaultTransformations),
|
||||
customIcon = it,
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
return suggestions
|
||||
|
||||
}
|
||||
|
||||
suspend fun getUncustomizedDefaultIcon(
|
||||
searchable: SavableSearchable,
|
||||
size: Int
|
||||
): CustomIconWithPreview? {
|
||||
val icon = iconProviders.first().getFirstIcon(searchable, size)
|
||||
?.transform(transformations.first()) ?: return null
|
||||
return CustomIconWithPreview(
|
||||
customIcon = null,
|
||||
preview = icon
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun searchCustomIcons(query: String): List<CustomIconWithPreview> {
|
||||
val transformations = this.transformations.first()
|
||||
val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
|
||||
val componentName = it.componentName ?: return@mapNotNull null
|
||||
|
||||
CustomIconWithPreview(
|
||||
customIcon = CustomIconPackIcon(
|
||||
iconPackPackage = it.iconPack,
|
||||
iconComponentName = componentName.flattenToString(),
|
||||
),
|
||||
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: SavableSearchable, icon: CustomIcon?) {
|
||||
customAttributesRepository.setCustomIcon(searchable, icon)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class CustomIconWithPreview(
|
||||
val preview: LauncherIcon,
|
||||
val customIcon: CustomIcon?,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val iconsModule = module {
|
||||
single { IconPackManager(androidContext(), get()) }
|
||||
single { IconRepository(androidContext(), get(), get(), get()) }
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package de.mm20.launcher2.icons
|
||||
|
||||
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
|
||||
|
||||
internal interface TransformableDynamicLauncherIcon {
|
||||
fun setTransformations(transformations: List<LauncherIconTransformation>)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import de.mm20.launcher2.icons.DynamicCalendarIcon
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
|
||||
class CalendarIconProvider(val context: Context): IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
if(searchable !is LauncherApp) return null
|
||||
val component = ComponentName(searchable.`package`, searchable.activity)
|
||||
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 DynamicCalendarIcon(
|
||||
resources = resources,
|
||||
resourceIds = drawableIds
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.ComponentName
|
||||
import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
|
||||
import de.mm20.launcher2.icons.IconPackManager
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
class CustomIconPackIconProvider(
|
||||
private val customIcon: CustomIconPackIcon,
|
||||
private val iconPackManager: IconPackManager,
|
||||
) : IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
return iconPackManager.getIcon(
|
||||
customIcon.iconPackPackage,
|
||||
ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
|
||||
import de.mm20.launcher2.icons.IconPackManager
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
class CustomThemedIconProvider(
|
||||
private val customIcon: CustomThemedIcon,
|
||||
private val iconPackManager: IconPackManager,
|
||||
): IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.graphics.drawable.RotateDrawable
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
|
||||
class GoogleClockIconProvider(val context: Context) : IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
if (searchable !is LauncherApp) return null
|
||||
if (searchable.`package` != "com.google.android.deskclock") return null
|
||||
val pm = context.packageManager
|
||||
val appInfo = try {
|
||||
pm.getApplicationInfo(
|
||||
"com.google.android.deskclock",
|
||||
PackageManager.GET_META_DATA
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
val drawable =
|
||||
appInfo.metaData.getInt("com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND")
|
||||
val resources = pm.getResourcesForApplication(appInfo)
|
||||
val baseIcon = try {
|
||||
ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable
|
||||
?: return null
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
val foreground = baseIcon.foreground as? LayerDrawable ?: return null
|
||||
val hourLayer =
|
||||
appInfo.metaData.getInt("com.android.launcher3.HOUR_LAYER_INDEX")
|
||||
val minuteLayer =
|
||||
appInfo.metaData.getInt("com.android.launcher3.MINUTE_LAYER_INDEX")
|
||||
val secondLayer =
|
||||
appInfo.metaData.getInt("com.android.launcher3.SECOND_LAYER_INDEX")
|
||||
|
||||
val defaultHour =
|
||||
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_HOUR")
|
||||
val defaultMinute =
|
||||
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_MINUTE")
|
||||
val defaultSecond =
|
||||
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_SECOND")
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = ClockLayer(
|
||||
sublayers = (0 until foreground.numberOfLayers).map {
|
||||
val drw = foreground.getDrawable(it)
|
||||
if (drw is RotateDrawable) {
|
||||
drw.level = when (it) {
|
||||
hourLayer -> {
|
||||
(12 - defaultHour) * 60
|
||||
}
|
||||
minuteLayer -> {
|
||||
(60 - defaultMinute)
|
||||
}
|
||||
secondLayer -> {
|
||||
(60 - defaultSecond) * 10
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
ClockSublayer(
|
||||
drawable = drw,
|
||||
role = when (it) {
|
||||
hourLayer -> ClockSublayerRole.Hour
|
||||
minuteLayer -> ClockSublayerRole.Minute
|
||||
secondLayer -> ClockSublayerRole.Second
|
||||
else -> ClockSublayerRole.Static
|
||||
}
|
||||
)
|
||||
},
|
||||
scale = 1.5f,
|
||||
),
|
||||
backgroundLayer = StaticIconLayer(
|
||||
icon = baseIcon.background,
|
||||
scale = 1.5f,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class IconPackIconProvider(
|
||||
private val context: Context,
|
||||
private val iconPack: String,
|
||||
private val iconPackManager: IconPackManager,
|
||||
): IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
if (searchable !is LauncherApp) return null
|
||||
|
||||
val component = ComponentName(searchable.`package`, searchable.activity)
|
||||
return iconPackManager.getIcon(iconPack, component)
|
||||
?: iconPackManager.generateIcon(
|
||||
context,
|
||||
iconPack,
|
||||
baseIcon = withContext(Dispatchers.IO) {
|
||||
searchable.launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)
|
||||
},
|
||||
size = size,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
interface IconProvider {
|
||||
suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon?
|
||||
}
|
||||
|
||||
internal suspend fun Iterable<IconProvider>.getFirstIcon(
|
||||
searchable: SavableSearchable,
|
||||
size: Int
|
||||
): LauncherIcon? {
|
||||
for (provider in this) {
|
||||
val icon = provider.getIcon(searchable, size)
|
||||
if (icon != null) {
|
||||
return icon
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
class PlaceholderIconProvider(val context: Context) : IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon {
|
||||
return searchable.getPlaceholderIcon(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
class SystemIconProvider(
|
||||
private val context: Context,
|
||||
private val themedIcons: Boolean,
|
||||
) : IconProvider {
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
return searchable.loadIcon(context, size, themedIcons)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
|
||||
internal class ThemedIconProvider(
|
||||
private val iconPackManager: IconPackManager,
|
||||
) : IconProvider {
|
||||
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? {
|
||||
if (searchable !is LauncherApp) return null
|
||||
return iconPackManager.getThemedIcon(searchable.`package`)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
internal class ThemedPlaceholderIconProvider(
|
||||
private val context: Context,
|
||||
) : IconProvider {
|
||||
|
||||
override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon {
|
||||
val icon = searchable.getPlaceholderIcon(context)
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = asThemed(icon.foregroundLayer),
|
||||
backgroundLayer = asThemed(icon.backgroundLayer),
|
||||
)
|
||||
}
|
||||
|
||||
private fun asThemed(layer: LauncherIconLayer): LauncherIconLayer {
|
||||
return when (layer) {
|
||||
is ClockLayer -> TintedClockLayer(
|
||||
scale = layer.scale,
|
||||
color = 0,
|
||||
sublayers = layer.sublayers,
|
||||
)
|
||||
is ColorLayer -> layer.copy(color = 0)
|
||||
is StaticIconLayer -> TintedIconLayer(
|
||||
icon = layer.icon,
|
||||
color = 0,
|
||||
scale = layer.scale,
|
||||
)
|
||||
is TextLayer -> layer.copy(color = 0)
|
||||
is TintedIconLayer -> layer.copy(color = 0)
|
||||
is TintedClockLayer -> return layer.copy(color = 0)
|
||||
is TransparentLayer -> return layer
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.mm20.launcher2.icons.transformations
|
||||
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TransformableDynamicLauncherIcon
|
||||
|
||||
internal interface LauncherIconTransformation {
|
||||
suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon
|
||||
}
|
||||
|
||||
internal suspend fun LauncherIcon.transform(transformations: Iterable<LauncherIconTransformation>): LauncherIcon {
|
||||
if (this is StaticLauncherIcon) {
|
||||
var transformedIcon = this
|
||||
for (transformation in transformations) {
|
||||
transformedIcon = transformation.transform(transformedIcon as StaticLauncherIcon)
|
||||
}
|
||||
return transformedIcon
|
||||
}
|
||||
if (this is TransformableDynamicLauncherIcon) {
|
||||
this.setTransformations(transformations.toList())
|
||||
return this
|
||||
}
|
||||
return this
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package de.mm20.launcher2.icons.transformations
|
||||
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.palette.graphics.Palette
|
||||
import de.mm20.launcher2.icons.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class LegacyToAdaptiveTransformation(
|
||||
private val foregroundScale: Float = 0.7f,
|
||||
private val backgroundColor: Int = 1,
|
||||
): LauncherIconTransformation {
|
||||
override suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon {
|
||||
if (icon.backgroundLayer !is TransparentLayer) return icon
|
||||
|
||||
val bgColor = if (backgroundColor == 1) extractColor(icon.foregroundLayer) else backgroundColor
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = scale(icon.foregroundLayer, foregroundScale),
|
||||
backgroundLayer = ColorLayer(bgColor)
|
||||
)
|
||||
}
|
||||
|
||||
private fun scale(layer: LauncherIconLayer, scale: Float): LauncherIconLayer {
|
||||
return when(layer) {
|
||||
is ClockLayer -> layer.copy(scale = scale)
|
||||
is StaticIconLayer -> layer.copy(scale = scale)
|
||||
is TintedClockLayer -> layer.copy(scale = scale)
|
||||
is TintedIconLayer -> layer.copy(scale = scale)
|
||||
else -> layer
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun extractColor(layer: LauncherIconLayer): Int {
|
||||
|
||||
if (layer is StaticIconLayer) {
|
||||
val drawable = layer.icon
|
||||
val bitmap = if (drawable is BitmapDrawable) {
|
||||
drawable.bitmap
|
||||
} else {
|
||||
drawable.toBitmap(48, 48)
|
||||
}
|
||||
|
||||
val palette = withContext(Dispatchers.Default) {
|
||||
Palette.from(bitmap).generate()
|
||||
}
|
||||
return palette.getDominantColor(0)
|
||||
} else if (layer is ColorLayer) {
|
||||
return layer.color
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user