add config icon feature

This commit is contained in:
2026-10-09 22:41:45 +08:00
parent 493c70ce81
commit aa028266ee
1621 changed files with 5130 additions and 77 deletions
File diff suppressed because it is too large Load Diff
@@ -117,6 +117,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
AndroidUiHost.activity = this
com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidAppIconArtwork.applyTaskIcon(this)
AppUtils.getSchemeData(intent) { url ->
PlatformServices.deepLink.handle(url)
}
@@ -53,6 +53,10 @@ class DesktopLyricOverlay(
@Volatile
private var cachedCurrentLine: Int = -1
fun refreshAppIcon() {
mainHandler.post { activeRef?.get()?.applyAppIcon() }
}
fun updateLyric(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
if (lyricLine1 != null) cachedLine1 = lyricLine1
if (lyricLine2 != null) cachedLine2 = lyricLine2
@@ -147,6 +151,12 @@ class DesktopLyricOverlay(
}
}
private fun applyAppIcon() {
val bitmap = com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidAppIconArtwork.bitmap(context)
ivIcon?.background = null
if (bitmap == null) ivIcon?.setImageResource(R.drawable.logo) else ivIcon?.setImageBitmap(bitmap)
}
@SuppressLint("ClickableViewAccessibility")
private fun bindView() {
val rl = root ?: return
@@ -157,6 +167,7 @@ class DesktopLyricOverlay(
ivClose = rl.findViewById(R.id.ic_close)
ivPip = rl.findViewById(R.id.ic_pip)
ivIcon = rl.findViewById(R.id.ic_icon)
applyAppIcon()
ivTranslate = rl.findViewById(R.id.ic_translate)
applyCachedToViews()
@@ -179,8 +179,9 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
@Composable
open fun RenderLogo(size: Dp, paddingInDp: Dp) {
val bitmap = com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidAppIconArtwork.bitmap(LocalContext.current, dark = !isWhite)
Image(
provider = ImageProvider(R.drawable.logo),
provider = if (bitmap == null) ImageProvider(R.drawable.logo) else ImageProvider(bitmap),
contentDescription = "logo",
modifier = GlanceModifier
.size(size)
@@ -0,0 +1,59 @@
package com.zhushenwudi.lovelivemusicplayer.native.platform
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.AtomicFile
import com.zhushenwudi.lovelivemusicplayer.appicon.AppIconCatalog
import java.io.File
/** Persist previews independently of the Activity so widgets can render after process death. */
object AndroidAppIconArtwork {
private fun prefs(context: Context) = context.getSharedPreferences("llmp_icon_artwork", Context.MODE_PRIVATE)
private fun directory(context: Context, id: String) = File(context.filesDir, "app-icon-artwork/$id")
private fun isDark(context: Context) = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
fun file(context: Context, dark: Boolean = isDark(context)): File? {
val id = prefs(context).getString("id", null) ?: return null
if (!AppIconCatalog.isValid(id) || id == AppIconCatalog.DEFAULT) return null
return File(directory(context, id), if (dark) "dark.png" else "light.png").takeIf { it.isFile }
}
fun bitmap(context: Context, dark: Boolean = isDark(context)): Bitmap? =
file(context, dark)?.let { BitmapFactory.decodeFile(it.path) }
@Suppress("DEPRECATION")
fun applyTaskIcon(activity: Activity) {
val label = activity.applicationInfo.loadLabel(activity.packageManager).toString()
val image = bitmap(activity)
activity.setTaskDescription(if (image == null) ActivityManager.TaskDescription(label) else ActivityManager.TaskDescription(label, image))
}
fun publish(context: Context, id: String, light: ByteArray?, dark: ByteArray?): Boolean {
require(AppIconCatalog.isValid(id))
var changed = false
if (id != AppIconCatalog.DEFAULT) {
require(light != null && dark != null)
val dir = directory(context, id)
check(dir.isDirectory || dir.mkdirs())
listOf("light.png" to light, "dark.png" to dark).forEach { (name, bytes) ->
val file = File(dir, name)
if (!file.isFile || !file.readBytes().contentEquals(bytes)) {
val atomic = AtomicFile(file)
val stream = atomic.startWrite()
try { stream.write(bytes); atomic.finishWrite(stream) }
catch (error: Exception) { atomic.failWrite(stream); throw error }
changed = true
}
}
}
val prefs = prefs(context)
val systemDark = isDark(context)
changed = changed || prefs.getString("id", null) != id || prefs.getBoolean("dark", !systemDark) != systemDark
check(prefs.edit().putString("id", id).putBoolean("dark", systemDark).commit())
return changed
}
}
@@ -0,0 +1,72 @@
package com.zhushenwudi.lovelivemusicplayer.native.platform
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.zhushenwudi.lovelivemusicplayer.appicon.AppIconCatalog
import com.zhushenwudi.lovelivemusicplayer.appicon.AppIconController
class AndroidAppIconController(context: Context) : AppIconController {
private val context = context.applicationContext
private val pm = context.packageManager
override val supported = true
override val appearanceHintKey = "app_icon_android_hint"
private val aliases = listOf("Default") + AppIconCatalog.characters.flatMap {
listOf(it.id.replace('-', '_') + "_light", it.id.replace('-', '_') + "_dark")
}
private fun component(alias: String) = ComponentName(context.packageName, "${context.packageName}.icon.$alias")
private fun enabled(alias: String): Boolean = when (pm.getComponentEnabledSetting(component(alias))) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> alias == "Default"
else -> false
}
override suspend fun publishArtwork(characterId: String, light: ByteArray?, dark: ByteArray?) {
val changed = withContext(Dispatchers.IO) { AndroidAppIconArtwork.publish(context, characterId, light, dark) }
if (changed) {
withContext(Dispatchers.Main) {
com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost.activity?.let(AndroidAppIconArtwork::applyTaskIcon)
}
com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices.homeWidget.refresh()
com.zhushenwudi.lovelivemusicplayer.native.DesktopLyricOverlay.refreshAppIcon()
}
}
override suspend fun currentCharacterId(): String? = withContext(Dispatchers.IO) {
val active = aliases.filter(::enabled).singleOrNull() ?: return@withContext null
if (active == "Default") AppIconCatalog.DEFAULT else
active.removeSuffix("_light").removeSuffix("_dark").replace('_', '-')
}
override suspend fun apply(characterId: String, systemDark: Boolean): Unit = withContext(Dispatchers.IO) {
require(AppIconCatalog.isValid(characterId))
val target = if (characterId == AppIconCatalog.DEFAULT) "Default" else
characterId.replace('-', '_') + if (systemDark) "_dark" else "_light"
val changes = aliases.filter { enabled(it) != (it == target) }
if (changes.isEmpty()) return@withContext
val previous = changes.associateWith { pm.getComponentEnabledSetting(component(it)) }
try {
if (Build.VERSION.SDK_INT >= 33) {
pm.setComponentEnabledSettings(changes.map {
PackageManager.ComponentEnabledSetting(component(it),
if (it == target) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
})
} else {
// Keep at least one launcher entry enabled throughout the transition.
pm.setComponentEnabledSetting(component(target), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
changes.filter { it != target }.forEach {
pm.setComponentEnabledSetting(component(it), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
}
}
check(aliases.filter(::enabled) == listOf(target)) { "Launcher icon change did not complete" }
} catch (error: Exception) {
try {
// Re-enable the previous launchers before disabling the failed target.
previous.entries.sortedBy { if (it.value == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || (it.value == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT && it.key == "Default")) 0 else 1 }
.forEach { (alias, state) -> pm.setComponentEnabledSetting(component(alias), state, PackageManager.DONT_KILL_APP) }
} catch (rollback: Exception) { error.addSuppressed(rollback) }
throw error
}
}
}
@@ -69,7 +69,9 @@ class AndroidShareBridge(
putString(QQShare.SHARE_TO_QQ_TARGET_URL, url)
putString(QQShare.SHARE_TO_QQ_APP_NAME, "LLMP")
val preview = imageUrl?.takeIf { it.startsWith("http") } ?: Const.SHARE_DEFAULT_LOGO
putString(QQShare.SHARE_TO_QQ_IMAGE_URL, preview)
val localIcon = if (preview == Const.SHARE_DEFAULT_LOGO) AndroidAppIconArtwork.file(appContext) else null
if (localIcon != null) putString(QQShare.SHARE_TO_QQ_IMAGE_LOCAL_URL, localIcon.path)
else putString(QQShare.SHARE_TO_QQ_IMAGE_URL, preview)
}
runCatching {
sdk.shareToQQ(activity, params, listener)
@@ -14,6 +14,7 @@ fun installAndroidPlatformServices(context: Context) {
UmengBridge.attachContext(appContext)
PlatformServices.install(
appIcon = AndroidAppIconController(appContext),
desktopLyric = AndroidDesktopLyricController(appContext),
homeWidget = AndroidHomeWidgetController(appContext),
deepLink = AndroidDeepLinkHandler(),
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Some files were not shown because too many files have changed in this diff Show More