add config icon feature
@@ -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(),
|
||||
|
||||
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 55 KiB |