Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa028266ee | ||
|
|
493c70ce81 | ||
|
|
fa61bcca38 | ||
|
|
7b856f574e | ||
|
|
53897bda1a | ||
|
|
52ab485e9c |
@@ -117,6 +117,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
AndroidUiHost.activity = this
|
AndroidUiHost.activity = this
|
||||||
|
com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidAppIconArtwork.applyTaskIcon(this)
|
||||||
AppUtils.getSchemeData(intent) { url ->
|
AppUtils.getSchemeData(intent) { url ->
|
||||||
PlatformServices.deepLink.handle(url)
|
PlatformServices.deepLink.handle(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ class DesktopLyricOverlay(
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var cachedCurrentLine: Int = -1
|
private var cachedCurrentLine: Int = -1
|
||||||
|
|
||||||
|
fun refreshAppIcon() {
|
||||||
|
mainHandler.post { activeRef?.get()?.applyAppIcon() }
|
||||||
|
}
|
||||||
|
|
||||||
fun updateLyric(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
|
fun updateLyric(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
|
||||||
if (lyricLine1 != null) cachedLine1 = lyricLine1
|
if (lyricLine1 != null) cachedLine1 = lyricLine1
|
||||||
if (lyricLine2 != null) cachedLine2 = lyricLine2
|
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")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun bindView() {
|
private fun bindView() {
|
||||||
val rl = root ?: return
|
val rl = root ?: return
|
||||||
@@ -157,6 +167,7 @@ class DesktopLyricOverlay(
|
|||||||
ivClose = rl.findViewById(R.id.ic_close)
|
ivClose = rl.findViewById(R.id.ic_close)
|
||||||
ivPip = rl.findViewById(R.id.ic_pip)
|
ivPip = rl.findViewById(R.id.ic_pip)
|
||||||
ivIcon = rl.findViewById(R.id.ic_icon)
|
ivIcon = rl.findViewById(R.id.ic_icon)
|
||||||
|
applyAppIcon()
|
||||||
ivTranslate = rl.findViewById(R.id.ic_translate)
|
ivTranslate = rl.findViewById(R.id.ic_translate)
|
||||||
applyCachedToViews()
|
applyCachedToViews()
|
||||||
|
|
||||||
|
|||||||
@@ -179,8 +179,9 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
open fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
open fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
||||||
|
val bitmap = com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidAppIconArtwork.bitmap(LocalContext.current, dark = !isWhite)
|
||||||
Image(
|
Image(
|
||||||
provider = ImageProvider(R.drawable.logo),
|
provider = if (bitmap == null) ImageProvider(R.drawable.logo) else ImageProvider(bitmap),
|
||||||
contentDescription = "logo",
|
contentDescription = "logo",
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.size(size)
|
.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_TARGET_URL, url)
|
||||||
putString(QQShare.SHARE_TO_QQ_APP_NAME, "LLMP")
|
putString(QQShare.SHARE_TO_QQ_APP_NAME, "LLMP")
|
||||||
val preview = imageUrl?.takeIf { it.startsWith("http") } ?: Const.SHARE_DEFAULT_LOGO
|
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 {
|
runCatching {
|
||||||
sdk.shareToQQ(activity, params, listener)
|
sdk.shareToQQ(activity, params, listener)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ fun installAndroidPlatformServices(context: Context) {
|
|||||||
UmengBridge.attachContext(appContext)
|
UmengBridge.attachContext(appContext)
|
||||||
|
|
||||||
PlatformServices.install(
|
PlatformServices.install(
|
||||||
|
appIcon = AndroidAppIconController(appContext),
|
||||||
desktopLyric = AndroidDesktopLyricController(appContext),
|
desktopLyric = AndroidDesktopLyricController(appContext),
|
||||||
homeWidget = AndroidHomeWidgetController(appContext),
|
homeWidget = AndroidHomeWidgetController(appContext),
|
||||||
deepLink = AndroidDeepLinkHandler(),
|
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 |