fix android desktop card bugs
This commit is contained in:
@@ -18,6 +18,8 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import com.zhushenwudi.lovelivemusicplayer.db.DriverFactory
|
import com.zhushenwudi.lovelivemusicplayer.db.DriverFactory
|
||||||
import com.zhushenwudi.lovelivemusicplayer.db.createDatabase
|
import com.zhushenwudi.lovelivemusicplayer.db.createDatabase
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetCommand
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.executeHomeWidgetCommand
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidShareBridge
|
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidShareBridge
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidDesktopLyricController
|
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidDesktopLyricController
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.installAndroidPlatformServices
|
import com.zhushenwudi.lovelivemusicplayer.native.platform.installAndroidPlatformServices
|
||||||
@@ -39,6 +41,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
initAndroidFileSystem(applicationContext)
|
initAndroidFileSystem(applicationContext)
|
||||||
|
|
||||||
if (!AppContainer.isInitialized) {
|
if (!AppContainer.isInitialized) {
|
||||||
|
// Install before init() starts observing/restoring playback state.
|
||||||
|
installAndroidPlatformServices(applicationContext)
|
||||||
val settings = createAppSettings(applicationContext)
|
val settings = createAppSettings(applicationContext)
|
||||||
val database = createDatabase(DriverFactory(applicationContext))
|
val database = createDatabase(DriverFactory(applicationContext))
|
||||||
player = PlayerController(applicationContext)
|
player = PlayerController(applicationContext)
|
||||||
@@ -52,7 +56,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
player = AppContainer.player
|
player = AppContainer.player
|
||||||
}
|
}
|
||||||
|
|
||||||
installAndroidPlatformServices(applicationContext)
|
|
||||||
val lyricEnabled = AppContainer.settings.enableDesktopLyric
|
val lyricEnabled = AppContainer.settings.enableDesktopLyric
|
||||||
if (!PlatformServices.desktopLyric.pipAutoOpen(lyricEnabled) && lyricEnabled) {
|
if (!PlatformServices.desktopLyric.pipAutoOpen(lyricEnabled) && lyricEnabled) {
|
||||||
AppContainer.settings.enableDesktopLyric = false
|
AppContainer.settings.enableDesktopLyric = false
|
||||||
@@ -61,9 +64,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
getSharedPreferences("my_prefs", MODE_PRIVATE).getBoolean("key_agreed", false),
|
getSharedPreferences("my_prefs", MODE_PRIVATE).getBoolean("key_agreed", false),
|
||||||
)
|
)
|
||||||
|
|
||||||
getSharedPreferences(AppUtils.SP_NAME, MODE_PRIVATE).edit(commit = true) {
|
PlatformServices.homeWidget.setShutdown(false)
|
||||||
putBoolean("isShutdown", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleIncomingIntent(intent)
|
handleIncomingIntent(intent)
|
||||||
|
|
||||||
@@ -162,15 +163,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWidgetUri(uri: String) {
|
private fun handleWidgetUri(uri: String) {
|
||||||
when {
|
val command = HomeWidgetCommand.fromValue(uri.removePrefix(AppUtils.HOME_WIDGET_URI_PREFIX)) ?: return
|
||||||
uri.endsWith("toggle_play") -> player?.toggle()
|
executeHomeWidgetCommand(this, command)
|
||||||
uri.endsWith("next") -> player?.next()
|
|
||||||
uri.endsWith("previous") -> player?.previous()
|
|
||||||
uri.endsWith("toggle_love") -> {
|
|
||||||
val id = player?.state?.value?.current?.musicId ?: return
|
|
||||||
AppContainer.toggleLove(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestNotifyPermission() {
|
private fun requestNotifyPermission() {
|
||||||
|
|||||||
+83
-145
@@ -8,9 +8,10 @@ import androidx.glance.LocalContext
|
|||||||
import androidx.glance.action.Action
|
import androidx.glance.action.Action
|
||||||
import androidx.glance.appwidget.action.actionStartActivity
|
import androidx.glance.appwidget.action.actionStartActivity
|
||||||
import com.zhushenwudi.lovelivemusicplayer.MainActivity
|
import com.zhushenwudi.lovelivemusicplayer.MainActivity
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
@@ -51,70 +52,32 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
open var radius: Dp = 6.dp
|
open var radius: Dp = 6.dp
|
||||||
open val cdSize = 80.dp
|
open val cdSize = 80.dp
|
||||||
abstract var isWhite: Boolean
|
abstract var isWhite: Boolean
|
||||||
var isPlaying = true
|
|
||||||
var isShutdown = false
|
|
||||||
private var isFavorite = false
|
|
||||||
private lateinit var sp: SharedPreferences
|
|
||||||
private lateinit var songName: String
|
|
||||||
private lateinit var songArtist: String
|
|
||||||
private lateinit var playText: String
|
|
||||||
|
|
||||||
private var musicColor = Color.White
|
private fun openAppAction(context: Context): Action {
|
||||||
private var coverPath: String? = null
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun widgetAction(context: Context, uri: String): Action {
|
|
||||||
val intent = Intent(context, MainActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
data = uri.toUri()
|
data = "${AppUtils.HOME_WIDGET_URI_PREFIX}open".toUri()
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||||
|
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
}
|
}
|
||||||
return actionStartActivity(intent)
|
return actionStartActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlanceContent(preferences: SharedPreferences) {
|
private fun controlAction(state: HomeWidgetState, command: HomeWidgetCommand): Action {
|
||||||
sp = preferences
|
val context = LocalContext.current
|
||||||
songName = sp.getString("songName", "")!!
|
return homeWidgetControlAction(command, state.controlsEnabled) { openAppAction(context) }
|
||||||
songArtist = sp.getString("songArtist", "")!!
|
|
||||||
isFavorite = sp.getBoolean("songFavorite", false)
|
|
||||||
isPlaying = sp.getBoolean("isPlaying", false)
|
|
||||||
playText = sp.getString("playText", "")!!
|
|
||||||
isShutdown = sp.getBoolean("isShutdown", false)
|
|
||||||
coverPath = sp.getString("shareImage", null)
|
|
||||||
val strBgColor = sp.getString("bgColor", "")!!
|
|
||||||
|
|
||||||
coverPath?.let {
|
|
||||||
if (!File(it).exists()) {
|
|
||||||
coverPath = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strBgColor.contains(",")) {
|
|
||||||
val bgColorArr = strBgColor.split(",")
|
|
||||||
musicColor = Color(
|
|
||||||
red = bgColorArr[0].toDoubleOrNull()?.toInt() ?: 1,
|
|
||||||
green = bgColorArr[1].toDoubleOrNull()?.toInt() ?: 1,
|
|
||||||
blue = bgColorArr[2].toDoubleOrNull()?.toInt() ?: 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isShutdown) {
|
|
||||||
isPlaying = false
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderAppWidget()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderAppWidget() {
|
fun RenderAppWidget(state: HomeWidgetState) {
|
||||||
Box(
|
Box(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.width(size.width)
|
.width(size.width)
|
||||||
.height(size.height)
|
.height(size.height)
|
||||||
) {
|
) {
|
||||||
if (isWhite) {
|
if (isWhite) {
|
||||||
GradientRectangle(color = musicColor)
|
GradientRectangle(color = state.musicColor)
|
||||||
} else {
|
} else {
|
||||||
Box(
|
Box(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
@@ -122,6 +85,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
.width(size.width)
|
.width(size.width)
|
||||||
.height(size.height)
|
.height(size.height)
|
||||||
.cornerRadius(radius)
|
.cornerRadius(radius)
|
||||||
|
.clickable(openAppAction(LocalContext.current))
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +102,9 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
.size(size.height)
|
.size(size.height)
|
||||||
.cornerRadius(radius)
|
.cornerRadius(radius)
|
||||||
) {
|
) {
|
||||||
RenderCD(isLargePauseWidget = false)
|
RenderCD(state, isLargePauseWidget = false)
|
||||||
RenderCover(isLargePauseWidget = false)
|
RenderCover(state, isLargePauseWidget = false)
|
||||||
RenderPlayController(false)
|
RenderPlayController(state, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,20 +120,21 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
RenderLogo(size = 22.dp, paddingInDp = (-6).dp)
|
RenderLogo(size = 22.dp, paddingInDp = (-6).dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderTextArr(stateFontSize = 9.sp, infoFontSize = 11.sp)
|
RenderTextArr(state, stateFontSize = 9.sp, infoFontSize = 11.sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderFavorite(imageSize = 20.dp, paddingInDp = radius)
|
RenderFavorite(state, imageSize = 20.dp, paddingInDp = radius)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun calcCdOffsetX(): Dp = cdSize / 2 - 10.dp
|
open fun calcCdOffsetX(state: HomeWidgetState): Dp = cdSize / 2 - 10.dp
|
||||||
|
|
||||||
open fun calcCdOffsetY(): Dp = cdSize / 2 - 10.dp
|
open fun calcCdOffsetY(state: HomeWidgetState): Dp = cdSize / 2 - 10.dp
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@Composable
|
@Composable
|
||||||
open fun RenderTextArr(
|
open fun RenderTextArr(
|
||||||
|
state: HomeWidgetState,
|
||||||
stateFontSize: TextUnit,
|
stateFontSize: TextUnit,
|
||||||
infoFontSize: TextUnit
|
infoFontSize: TextUnit
|
||||||
) {
|
) {
|
||||||
@@ -178,13 +143,8 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
} else {
|
} else {
|
||||||
size.width - 75.dp
|
size.width - 75.dp
|
||||||
}
|
}
|
||||||
val playTextArr = playText.split(",")
|
|
||||||
var playStateText = playTextArr[1]
|
|
||||||
if (!isShutdown && isPlaying) {
|
|
||||||
playStateText = playTextArr[0]
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = playStateText,
|
text = state.playStateText,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = stateFontSize,
|
fontSize = stateFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -195,7 +155,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = songName,
|
text = state.songName,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = infoFontSize,
|
fontSize = infoFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -206,7 +166,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = songArtist,
|
text = state.songArtist,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = infoFontSize,
|
fontSize = infoFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -225,26 +185,24 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.padding(start = paddingInDp)
|
.padding(start = paddingInDp)
|
||||||
|
.clickable(openAppAction(LocalContext.current))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderTransportButton(
|
private fun RenderTransportButton(
|
||||||
|
state: HomeWidgetState,
|
||||||
drawable: Int,
|
drawable: Int,
|
||||||
description: String,
|
description: String,
|
||||||
uri: String,
|
command: HomeWidgetCommand,
|
||||||
imageSize: Dp,
|
imageSize: Dp,
|
||||||
) {
|
) {
|
||||||
val base = GlanceModifier.size(imageSize)
|
val modifier = GlanceModifier.size(imageSize)
|
||||||
val modifier = if (isShutdown) {
|
.clickable(
|
||||||
base
|
|
||||||
} else {
|
|
||||||
base.clickable(
|
|
||||||
rippleOverride = R.drawable.click_ripple,
|
rippleOverride = R.drawable.click_ripple,
|
||||||
onClick = widgetAction(LocalContext.current, uri),
|
onClick = controlAction(state, command),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -259,56 +217,45 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
open fun RenderCD(isLargePauseWidget: Boolean) {
|
open fun RenderCD(state: HomeWidgetState, isLargePauseWidget: Boolean) {
|
||||||
val cdDrawable = if (isWhite) R.drawable.cd_white else R.drawable.cd_black
|
val cdDrawable = if (isWhite) R.drawable.cd_white else R.drawable.cd_black
|
||||||
val offsetX = calcCdOffsetX()
|
val offsetX = calcCdOffsetX(state)
|
||||||
val offsetY = calcCdOffsetY()
|
val offsetY = calcCdOffsetY(state)
|
||||||
|
|
||||||
val startPadding = if (isLargePauseWidget) 0.dp else offsetX
|
val startPadding = if (isLargePauseWidget) 0.dp else offsetX
|
||||||
val radio = if (isLargePauseWidget) 0.9f else 1f
|
val radio = if (isLargePauseWidget) 0.9f else 1f
|
||||||
if (isShutdown) {
|
Image(
|
||||||
Image(
|
provider = ImageProvider(cdDrawable),
|
||||||
provider = ImageProvider(cdDrawable),
|
contentDescription = "cd",
|
||||||
contentDescription = "cd",
|
modifier = GlanceModifier
|
||||||
modifier = GlanceModifier
|
.size(size.height * radio)
|
||||||
.size(size.height * radio)
|
.padding(
|
||||||
.padding(
|
start = startPadding,
|
||||||
start = startPadding,
|
top = -offsetY,
|
||||||
top = -offsetY,
|
end = -offsetX,
|
||||||
end = -offsetX,
|
bottom = offsetY
|
||||||
bottom = offsetY
|
)
|
||||||
)
|
.clickable(
|
||||||
)
|
rippleOverride = R.drawable.click_ripple,
|
||||||
} else {
|
onClick = controlAction(state, HomeWidgetCommand.TOGGLE_PLAY),
|
||||||
Image(
|
)
|
||||||
provider = ImageProvider(cdDrawable),
|
)
|
||||||
contentDescription = "cd",
|
|
||||||
modifier = GlanceModifier
|
|
||||||
.size(size.height * radio)
|
|
||||||
.padding(
|
|
||||||
start = startPadding,
|
|
||||||
top = -offsetY,
|
|
||||||
end = -offsetX,
|
|
||||||
bottom = offsetY
|
|
||||||
)
|
|
||||||
.clickable(rippleOverride = R.drawable.click_ripple, onClick = widgetAction(LocalContext.current, "${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_play"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
open fun RenderCover(isLargePauseWidget: Boolean) {
|
open fun RenderCover(state: HomeWidgetState, isLargePauseWidget: Boolean) {
|
||||||
val coverSize = cdSize * 0.4f
|
val coverSize = cdSize * 0.4f
|
||||||
val coverAndCdDiffSize = (cdSize - coverSize) / 2
|
val coverAndCdDiffSize = (cdSize - coverSize) / 2
|
||||||
val offsetX = calcCdOffsetX()
|
val offsetX = calcCdOffsetX(state)
|
||||||
val offsetY = calcCdOffsetY()
|
val offsetY = calcCdOffsetY(state)
|
||||||
val startPadding =
|
val startPadding =
|
||||||
if (isLargePauseWidget) coverAndCdDiffSize else offsetX + coverAndCdDiffSize
|
if (isLargePauseWidget) coverAndCdDiffSize else offsetX + coverAndCdDiffSize
|
||||||
val radio = if (isLargePauseWidget) 0.95f else 1f
|
val radio = if (isLargePauseWidget) 0.95f else 1f
|
||||||
coverPath?.let {
|
state.coverPath?.let {
|
||||||
if (File(it).exists()) {
|
if (File(it).exists()) {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(it) ?: return
|
||||||
Image(
|
Image(
|
||||||
provider = ImageProvider(BitmapFactory.decodeFile(it)),
|
provider = ImageProvider(bitmap),
|
||||||
contentDescription = "cover",
|
contentDescription = "cover",
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.size(size.height * radio)
|
.size(size.height * radio)
|
||||||
@@ -325,21 +272,14 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@Composable
|
@Composable
|
||||||
open fun RenderPlayController(isLargePauseWidget: Boolean) {
|
open fun RenderPlayController(state: HomeWidgetState, isLargePauseWidget: Boolean) {
|
||||||
val resId = if (isPlaying) R.drawable.vector_pause else R.drawable.vector_play
|
val resId = if (state.isPlaying) R.drawable.vector_pause else R.drawable.vector_play
|
||||||
val toggleAction = widgetAction(
|
val toggleAction = controlAction(state, HomeWidgetCommand.TOGGLE_PLAY)
|
||||||
LocalContext.current,
|
|
||||||
"${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_play",
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isLargePauseWidget) {
|
if (isLargePauseWidget) {
|
||||||
val modifier = if (isShutdown) {
|
val modifier = GlanceModifier
|
||||||
GlanceModifier.size(20.dp)
|
.size(20.dp)
|
||||||
} else {
|
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
|
||||||
GlanceModifier
|
|
||||||
.size(20.dp)
|
|
||||||
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
|
|
||||||
}
|
|
||||||
Image(
|
Image(
|
||||||
provider = ImageProvider(resId),
|
provider = ImageProvider(resId),
|
||||||
contentDescription = "playButton",
|
contentDescription = "playButton",
|
||||||
@@ -349,13 +289,9 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
} else {
|
} else {
|
||||||
val padding =
|
val padding =
|
||||||
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) 26.dp else 23.dp
|
if (size.width == AppUtils.HORIZONTAL_RECTANGLE.width) 26.dp else 23.dp
|
||||||
val playModifier = if (isShutdown) {
|
val playModifier = GlanceModifier
|
||||||
GlanceModifier.size(15.dp)
|
.size(15.dp)
|
||||||
} else {
|
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
|
||||||
GlanceModifier
|
|
||||||
.size(15.dp)
|
|
||||||
.clickable(rippleOverride = R.drawable.click_ripple, onClick = toggleAction)
|
|
||||||
}
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.TopEnd,
|
contentAlignment = Alignment.TopEnd,
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
@@ -374,7 +310,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
open fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
|
open fun RenderFavorite(state: HomeWidgetState, imageSize: Dp, paddingInDp: Dp) {
|
||||||
val isLarge = size.width == AppUtils.HORIZONTAL_RECTANGLE.width
|
val isLarge = size.width == AppUtils.HORIZONTAL_RECTANGLE.width
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.BottomEnd,
|
contentAlignment = Alignment.BottomEnd,
|
||||||
@@ -386,35 +322,30 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (isLarge) {
|
if (isLarge) {
|
||||||
RenderTransportButton(
|
RenderTransportButton(
|
||||||
|
state = state,
|
||||||
drawable = R.drawable.player_play_prev,
|
drawable = R.drawable.player_play_prev,
|
||||||
description = "previous",
|
description = "previous",
|
||||||
uri = "${AppUtils.HOME_WIDGET_URI_PREFIX}previous",
|
command = HomeWidgetCommand.PREVIOUS,
|
||||||
imageSize = imageSize,
|
imageSize = imageSize,
|
||||||
)
|
)
|
||||||
Spacer(GlanceModifier.width(8.dp))
|
Spacer(GlanceModifier.width(8.dp))
|
||||||
}
|
}
|
||||||
RenderTransportButton(
|
RenderTransportButton(
|
||||||
|
state = state,
|
||||||
drawable = R.drawable.player_play_next,
|
drawable = R.drawable.player_play_next,
|
||||||
description = "next",
|
description = "next",
|
||||||
uri = "${AppUtils.HOME_WIDGET_URI_PREFIX}next",
|
command = HomeWidgetCommand.NEXT,
|
||||||
imageSize = imageSize,
|
imageSize = imageSize,
|
||||||
)
|
)
|
||||||
Spacer(GlanceModifier.width(8.dp))
|
Spacer(GlanceModifier.width(8.dp))
|
||||||
val loveIcon =
|
val loveIcon =
|
||||||
if (isFavorite) R.drawable.widget_fav_click else R.drawable.widget_fav_unclick
|
if (state.isFavorite) R.drawable.widget_fav_click else R.drawable.widget_fav_unclick
|
||||||
val favoriteModifier = if (isShutdown) {
|
val favoriteModifier = GlanceModifier
|
||||||
GlanceModifier.size(imageSize)
|
.size(imageSize)
|
||||||
} else {
|
.clickable(
|
||||||
GlanceModifier
|
rippleOverride = R.drawable.click_ripple,
|
||||||
.size(imageSize)
|
onClick = controlAction(state, HomeWidgetCommand.TOGGLE_FAVORITE),
|
||||||
.clickable(
|
)
|
||||||
rippleOverride = R.drawable.click_ripple,
|
|
||||||
onClick = widgetAction(
|
|
||||||
LocalContext.current,
|
|
||||||
"${AppUtils.HOME_WIDGET_URI_PREFIX}toggle_love",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Image(
|
Image(
|
||||||
provider = ImageProvider(loveIcon),
|
provider = ImageProvider(loveIcon),
|
||||||
contentDescription = "love",
|
contentDescription = "love",
|
||||||
@@ -433,6 +364,7 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
.width(size.width)
|
.width(size.width)
|
||||||
.height(size.height)
|
.height(size.height)
|
||||||
.cornerRadius(radius)
|
.cornerRadius(radius)
|
||||||
|
.clickable(openAppAction(LocalContext.current))
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
provider = ImageProvider(bitmap),
|
provider = ImageProvider(bitmap),
|
||||||
@@ -446,7 +378,13 @@ abstract class HomeWidgetContent : GlanceAppWidget() {
|
|||||||
|
|
||||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||||
val preferences = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
val preferences = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||||
provideContent { GlanceContent(preferences = preferences) }
|
val states = preferences.widgetStates()
|
||||||
|
val initialState = HomeWidgetState.fromPreferences(preferences.all)
|
||||||
|
provideContent {
|
||||||
|
val state by states.collectAsState(initialState)
|
||||||
|
val running by HomeWidgetRuntime.isRunning.collectAsState()
|
||||||
|
RenderAppWidget(state.withAppRunning(running))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package com.zhushenwudi.lovelivemusicplayer.native.home_widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.glance.GlanceId
|
||||||
|
import androidx.glance.action.Action
|
||||||
|
import androidx.glance.action.ActionParameters
|
||||||
|
import androidx.glance.action.actionParametersOf
|
||||||
|
import androidx.glance.appwidget.action.ActionCallback
|
||||||
|
import androidx.glance.appwidget.action.actionRunCallback
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidHomeWidgetController
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.player.PlaybackService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/** Process-local availability: a persisted widget snapshot cannot prove the player is alive. */
|
||||||
|
internal object HomeWidgetRuntime {
|
||||||
|
private val running = MutableStateFlow(false)
|
||||||
|
val isRunning = running.asStateFlow()
|
||||||
|
|
||||||
|
fun setRunning(value: Boolean) {
|
||||||
|
running.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class HomeWidgetCommand(val value: String) {
|
||||||
|
TOGGLE_PLAY("toggle_play"),
|
||||||
|
PREVIOUS("previous"),
|
||||||
|
NEXT("next"),
|
||||||
|
TOGGLE_FAVORITE("toggle_love");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val key = ActionParameters.Key<String>("widget_command")
|
||||||
|
|
||||||
|
fun fromValue(value: String?): HomeWidgetCommand? = entries.firstOrNull { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun homeWidgetControlAction(
|
||||||
|
command: HomeWidgetCommand,
|
||||||
|
controlsEnabled: Boolean,
|
||||||
|
openApp: () -> Action,
|
||||||
|
): Action = if (controlsEnabled) {
|
||||||
|
actionRunCallback<HomeWidgetPlaybackAction>(actionParametersOf(HomeWidgetCommand.key to command.value))
|
||||||
|
} else {
|
||||||
|
openApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Glance delivers this callback without bringing MainActivity to the foreground. */
|
||||||
|
class HomeWidgetPlaybackAction : ActionCallback {
|
||||||
|
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||||
|
val command = HomeWidgetCommand.fromValue(parameters[HomeWidgetCommand.key]) ?: return
|
||||||
|
withContext(Dispatchers.Main.immediate) {
|
||||||
|
val shutdown = context.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("isShutdown", false)
|
||||||
|
// The process may have died since RemoteViews were published. Do not replay playback
|
||||||
|
// commands on a cold start; refresh the controls into their open-app state instead.
|
||||||
|
if (AppContainer.isInitialized && HomeWidgetRuntime.isRunning.value && !shutdown) {
|
||||||
|
executeHomeWidgetCommand(context, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AndroidHomeWidgetController(context).refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Both legacy activity intents and widget callbacks execute ExoPlayer commands on the main thread. */
|
||||||
|
internal fun executeHomeWidgetCommand(context: Context, command: HomeWidgetCommand) {
|
||||||
|
if (!AppContainer.isInitialized) return
|
||||||
|
val player = AppContainer.player
|
||||||
|
when (command) {
|
||||||
|
HomeWidgetCommand.TOGGLE_PLAY -> {
|
||||||
|
if (!player.state.value.isPlaying) PlaybackService.start(context)
|
||||||
|
player.toggle()
|
||||||
|
}
|
||||||
|
HomeWidgetCommand.PREVIOUS -> player.previous()
|
||||||
|
HomeWidgetCommand.NEXT -> player.next()
|
||||||
|
HomeWidgetCommand.TOGGLE_FAVORITE -> {
|
||||||
|
val id = player.state.value.current?.musicId ?: return
|
||||||
|
AppContainer.toggleLove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.zhushenwudi.lovelivemusicplayer.native.home_widget
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
/** Immutable snapshots also invalidate an already running Glance composition. */
|
||||||
|
data class HomeWidgetState(
|
||||||
|
val songName: String = "",
|
||||||
|
val songArtist: String = "",
|
||||||
|
val isFavorite: Boolean = false,
|
||||||
|
val isPlaying: Boolean = false,
|
||||||
|
val isShutdown: Boolean = false,
|
||||||
|
val controlsEnabled: Boolean = false,
|
||||||
|
val playText: String = "Playing,Paused",
|
||||||
|
val coverPath: String? = null,
|
||||||
|
val backgroundColor: String = "",
|
||||||
|
) {
|
||||||
|
fun withAppRunning(running: Boolean): HomeWidgetState = copy(
|
||||||
|
controlsEnabled = running && !isShutdown,
|
||||||
|
isPlaying = isPlaying && running && !isShutdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
val playStateText: String
|
||||||
|
get() {
|
||||||
|
val labels = playText.split(',', limit = 2)
|
||||||
|
val index = if (isPlaying) 0 else 1
|
||||||
|
return labels.getOrNull(index)?.takeIf { it.isNotBlank() }
|
||||||
|
?: if (isPlaying) "Playing" else "Paused"
|
||||||
|
}
|
||||||
|
|
||||||
|
val musicColor: Color
|
||||||
|
get() {
|
||||||
|
val components = backgroundColor.split(',').map { it.toFloatOrNull() }
|
||||||
|
if (components.size != 3 || components.any { it == null || !it.isFinite() }) {
|
||||||
|
return Color.White
|
||||||
|
}
|
||||||
|
return Color(
|
||||||
|
red = components[0]!!.coerceIn(0f, 1f),
|
||||||
|
green = components[1]!!.coerceIn(0f, 1f),
|
||||||
|
blue = components[2]!!.coerceIn(0f, 1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromPreferences(values: Map<String, *>): HomeWidgetState {
|
||||||
|
val shutdown = values["isShutdown"] == true
|
||||||
|
return HomeWidgetState(
|
||||||
|
songName = values["songName"] as? String ?: "",
|
||||||
|
songArtist = values["songArtist"] as? String ?: "",
|
||||||
|
isFavorite = values["songFavorite"] == true,
|
||||||
|
isPlaying = values["isPlaying"] == true && !shutdown,
|
||||||
|
isShutdown = shutdown,
|
||||||
|
playText = values["playText"] as? String ?: "Playing,Paused",
|
||||||
|
coverPath = values["shareImage"] as? String,
|
||||||
|
backgroundColor = values["bgColor"] as? String ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SharedPreferences.widgetStates() = callbackFlow {
|
||||||
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { preferences, _ ->
|
||||||
|
trySend(HomeWidgetState.fromPreferences(preferences.all))
|
||||||
|
}
|
||||||
|
// Register first so a write during the initial read cannot be missed.
|
||||||
|
registerOnSharedPreferenceChangeListener(listener)
|
||||||
|
trySend(HomeWidgetState.fromPreferences(all))
|
||||||
|
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
|
||||||
|
}.conflate().distinctUntilChanged()
|
||||||
+15
-18
@@ -8,6 +8,7 @@ import androidx.compose.ui.unit.TextUnit
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetContent
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetContent
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetState
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||||
|
|
||||||
@SuppressLint("CommitPrefEdits", "DefaultLocale")
|
@SuppressLint("CommitPrefEdits", "DefaultLocale")
|
||||||
@@ -17,43 +18,39 @@ abstract class LargeHomeWidgetContent(
|
|||||||
override var cdSize: Dp = 120.dp,
|
override var cdSize: Dp = 120.dp,
|
||||||
) : HomeWidgetContent() {
|
) : HomeWidgetContent() {
|
||||||
|
|
||||||
private fun isLargePauseWidget(): Boolean {
|
|
||||||
return !isPlaying || isShutdown
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
override fun RenderLogo(size: Dp, paddingInDp: Dp) {
|
||||||
super.RenderLogo(size = 30.dp, paddingInDp)
|
super.RenderLogo(size = 30.dp, paddingInDp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun RenderCD(isLargePauseWidget: Boolean) {
|
override fun RenderCD(state: HomeWidgetState, isLargePauseWidget: Boolean) {
|
||||||
super.RenderCD(isLargePauseWidget = isLargePauseWidget())
|
super.RenderCD(state, isLargePauseWidget = !state.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun RenderCover(isLargePauseWidget: Boolean) {
|
override fun RenderCover(state: HomeWidgetState, isLargePauseWidget: Boolean) {
|
||||||
super.RenderCover(isLargePauseWidget = isLargePauseWidget())
|
super.RenderCover(state, isLargePauseWidget = !state.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun RenderPlayController(isLargePauseWidget: Boolean) {
|
override fun RenderPlayController(state: HomeWidgetState, isLargePauseWidget: Boolean) {
|
||||||
super.RenderPlayController(isLargePauseWidget = isLargePauseWidget())
|
super.RenderPlayController(state, isLargePauseWidget = !state.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun RenderTextArr(stateFontSize: TextUnit, infoFontSize: TextUnit) {
|
override fun RenderTextArr(state: HomeWidgetState, stateFontSize: TextUnit, infoFontSize: TextUnit) {
|
||||||
super.RenderTextArr(stateFontSize = 12.sp, infoFontSize = 13.sp)
|
super.RenderTextArr(state, stateFontSize = 12.sp, infoFontSize = 13.sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun RenderFavorite(imageSize: Dp, paddingInDp: Dp) {
|
override fun RenderFavorite(state: HomeWidgetState, imageSize: Dp, paddingInDp: Dp) {
|
||||||
super.RenderFavorite(imageSize = 28.dp, paddingInDp = radius / 2)
|
super.RenderFavorite(state, imageSize = 28.dp, paddingInDp = radius / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun calcCdOffsetX(): Dp =
|
override fun calcCdOffsetX(state: HomeWidgetState): Dp =
|
||||||
if (isPlaying) cdSize / 2 + 5.dp else 12.dp
|
if (state.isPlaying) cdSize / 2 + 5.dp else 12.dp
|
||||||
|
|
||||||
override fun calcCdOffsetY(): Dp =
|
override fun calcCdOffsetY(state: HomeWidgetState): Dp =
|
||||||
if (isPlaying) cdSize / 2 + 5.dp else 12.dp
|
if (state.isPlaying) cdSize / 2 + 5.dp else 12.dp
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-28
@@ -4,12 +4,15 @@ import android.appwidget.AppWidgetManager
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.HomeWidgetRuntime
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget.BlackLargeHomeWidgetReceiver
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget.BlackLargeHomeWidgetReceiver
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget.WhiteLargeHomeWidgetReceiver
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.large_home_widget.WhiteLargeHomeWidgetReceiver
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget.BlackSmallHomeWidgetReceiver
|
import com.zhushenwudi.lovelivemusicplayer.native.home_widget.small_home_widget.BlackSmallHomeWidgetReceiver
|
||||||
@@ -18,6 +21,8 @@ import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
|||||||
import com.zhushenwudi.lovelivemusicplayer.native.util.ImageUtil
|
import com.zhushenwudi.lovelivemusicplayer.native.util.ImageUtil
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.HomeWidgetController
|
import com.zhushenwudi.lovelivemusicplayer.platform.HomeWidgetController
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class AndroidHomeWidgetController(
|
class AndroidHomeWidgetController(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -25,6 +30,7 @@ class AndroidHomeWidgetController(
|
|||||||
private val appContext = context.applicationContext
|
private val appContext = context.applicationContext
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private var preImagePath: String? = null
|
private var preImagePath: String? = null
|
||||||
|
private var coverRevision = 0L
|
||||||
private var lastContent: WidgetContent? = null
|
private var lastContent: WidgetContent? = null
|
||||||
|
|
||||||
private val receiverClasses = listOf(
|
private val receiverClasses = listOf(
|
||||||
@@ -34,6 +40,7 @@ class AndroidHomeWidgetController(
|
|||||||
BlackLargeHomeWidgetReceiver::class.java,
|
BlackLargeHomeWidgetReceiver::class.java,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun update(
|
override fun update(
|
||||||
songName: String?,
|
songName: String?,
|
||||||
artist: String?,
|
artist: String?,
|
||||||
@@ -50,28 +57,38 @@ class AndroidHomeWidgetController(
|
|||||||
playText = playText,
|
playText = playText,
|
||||||
)
|
)
|
||||||
val contentChanged = content != lastContent
|
val contentChanged = content != lastContent
|
||||||
val coverChanged = !coverPath.isNullOrBlank() && coverPath != preImagePath
|
val requestedCover = coverPath?.takeIf { it.isNotBlank() }
|
||||||
|
val coverChanged = requestedCover != preImagePath || (lastContent == null && requestedCover == null)
|
||||||
if (!contentChanged && !coverChanged) return
|
if (!contentChanged && !coverChanged) return
|
||||||
|
|
||||||
if (contentChanged) {
|
if (coverChanged) {
|
||||||
lastContent = content
|
preImagePath = requestedCover
|
||||||
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
coverRevision++
|
||||||
.edit(commit = true) {
|
|
||||||
putString("songName", content.songName)
|
|
||||||
putString("songArtist", content.artist)
|
|
||||||
putBoolean("songFavorite", content.favorite)
|
|
||||||
putBoolean("isPlaying", content.isPlaying)
|
|
||||||
putString("playText", content.playText)
|
|
||||||
if (content.songName.isNotBlank() || content.isPlaying) {
|
|
||||||
putBoolean("isShutdown", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refresh()
|
|
||||||
}
|
}
|
||||||
if (coverChanged) coverPath?.let(::processCover)
|
lastContent = content
|
||||||
|
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit(commit = true) {
|
||||||
|
putString("songName", content.songName)
|
||||||
|
putString("songArtist", content.artist)
|
||||||
|
putBoolean("songFavorite", content.favorite)
|
||||||
|
putBoolean("isPlaying", content.isPlaying)
|
||||||
|
putString("playText", content.playText)
|
||||||
|
if (coverChanged || content.songName.isBlank()) {
|
||||||
|
remove("shareImage")
|
||||||
|
remove("bgColor")
|
||||||
|
}
|
||||||
|
if (content.songName.isNotBlank() || content.isPlaying) {
|
||||||
|
putBoolean("isShutdown", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
if (coverChanged && requestedCover != null) processCover(requestedCover, coverRevision)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun setShutdown(shutdown: Boolean) {
|
override fun setShutdown(shutdown: Boolean) {
|
||||||
|
HomeWidgetRuntime.setRunning(!shutdown)
|
||||||
|
lastContent = null
|
||||||
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||||
.edit(commit = true) {
|
.edit(commit = true) {
|
||||||
putBoolean("isShutdown", shutdown)
|
putBoolean("isShutdown", shutdown)
|
||||||
@@ -88,22 +105,45 @@ class AndroidHomeWidgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processCover(coverPath: String) {
|
private fun processCover(coverPath: String, revision: Long) {
|
||||||
if (preImagePath == coverPath || !File(coverPath).exists()) return
|
|
||||||
preImagePath = coverPath
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
var originBitmap: Bitmap? = null
|
||||||
|
var coverBitmap: Bitmap? = null
|
||||||
|
var coverFile: File? = null
|
||||||
|
var published = false
|
||||||
try {
|
try {
|
||||||
val originBitmap = BitmapFactory.decodeFile(coverPath)
|
originBitmap = BitmapFactory.decodeFile(coverPath) ?: return@launch
|
||||||
val coverBitmap = ImageUtil.squareAndCircularBitmap(originBitmap, 180)
|
coverBitmap = ImageUtil.squareAndCircularBitmap(originBitmap, 180)
|
||||||
val handledCoverPath = ImageUtil.saveBitmapToFile(appContext, coverBitmap)
|
|
||||||
ImageUtil.savePathToSp(appContext, handledCoverPath)
|
|
||||||
val color = ImageUtil.parsePhotoDomainColor(coverBitmap)
|
val color = ImageUtil.parsePhotoDomainColor(coverBitmap)
|
||||||
ImageUtil.saveColorToSp(appContext, color)
|
// Publish only complete files; keep one cached bitmap per source, not per update.
|
||||||
originBitmap.recycle()
|
val stagedFile = File(appContext.cacheDir, "widget-cover-${UUID.randomUUID()}.tmp")
|
||||||
coverBitmap.recycle()
|
coverFile = stagedFile
|
||||||
} catch (_: Exception) {
|
stagedFile.outputStream().use { output ->
|
||||||
|
check(coverBitmap.compress(Bitmap.CompressFormat.PNG, 100, output))
|
||||||
|
}
|
||||||
|
synchronized(this@AndroidHomeWidgetController) {
|
||||||
|
// A slow decode for a previous track must not replace the current artwork.
|
||||||
|
if (revision != coverRevision) return@launch
|
||||||
|
val cacheKey = UUID.nameUUIDFromBytes(coverPath.toByteArray(Charsets.UTF_8))
|
||||||
|
val cachedFile = File(appContext.cacheDir, "widget-cover-$cacheKey.png")
|
||||||
|
if (!cachedFile.exists()) check(stagedFile.renameTo(cachedFile))
|
||||||
|
val preferences = appContext.getSharedPreferences(AppUtils.SP_NAME, Context.MODE_PRIVATE)
|
||||||
|
preferences.edit(commit = true) {
|
||||||
|
putString("shareImage", cachedFile.absolutePath)
|
||||||
|
putString("bgColor", String.format(Locale.US, "%.4f,%.4f,%.4f", color.red, color.green, color.blue))
|
||||||
|
}
|
||||||
|
published = true
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.w("LLMP-Widget", "Unable to prepare widget cover", error)
|
||||||
} finally {
|
} finally {
|
||||||
refresh()
|
originBitmap?.recycle()
|
||||||
|
coverBitmap?.recycle()
|
||||||
|
coverFile?.delete()
|
||||||
|
if (!published) synchronized(this@AndroidHomeWidgetController) {
|
||||||
|
if (revision == coverRevision) preImagePath = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -167,6 +167,7 @@ class PlaybackService : MediaSessionService() {
|
|||||||
private var instance: PlaybackService? = null
|
private var instance: PlaybackService? = null
|
||||||
|
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
|
if (instance != null) return
|
||||||
val intent = Intent(context, PlaybackService::class.java)
|
val intent = Intent(context, PlaybackService::class.java)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -590,7 +590,7 @@ object AppContainer {
|
|||||||
homeWidgetJob = scope.launch {
|
homeWidgetJob = scope.launch {
|
||||||
player.state
|
player.state
|
||||||
.map { st ->
|
.map { st ->
|
||||||
Triple(st.current?.musicId, st.isPlaying, st.current?.isLove == true)
|
st.current to st.isPlaying
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect {
|
.collect {
|
||||||
@@ -675,13 +675,13 @@ object AppContainer {
|
|||||||
updateCover: Boolean = true,
|
updateCover: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val st = player.state.value
|
val st = player.state.value
|
||||||
val current = st.current ?: return
|
val current = st.current
|
||||||
val coverPath = if (updateCover) widgetCoverCache.resolveForWidget(current) else null
|
val coverPath = if (updateCover && current != null) widgetCoverCache.resolveForWidget(current) else null
|
||||||
PlatformServices.homeWidget.update(
|
PlatformServices.homeWidget.update(
|
||||||
songName = current.musicName,
|
songName = current?.musicName,
|
||||||
artist = current.artist,
|
artist = current?.artist,
|
||||||
isPlaying = st.isPlaying,
|
isPlaying = st.isPlaying,
|
||||||
favorite = current.isLove == true,
|
favorite = current?.isLove == true,
|
||||||
coverPath = coverPath,
|
coverPath = coverPath,
|
||||||
playText = I18n.t("play_text"),
|
playText = I18n.t("play_text"),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user