Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ internal class WidgetUpdateManager(
*/
fun clearCachedState() {
lastWidgetPlayerInfo = null
wearStatePublisher.clearCache()
}

private suspend fun processUpdateInternal() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ class WearStatePublisher @Inject constructor(
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val json = Json { ignoreUnknownKeys = true }

private val artworkCacheLock = Any()
private var lastArtworkUri: String? = null
private var lastRawBitmapData: ByteArray? = null
private var cachedWearArtAsset: Asset? = null

/**
* Clear the cached artwork assets.
*/
fun clearCache() {
synchronized(artworkCacheLock) {
lastArtworkUri = null
lastRawBitmapData = null
cachedWearArtAsset = null
}
Timber.tag(TAG).d("Cleared Wear artwork cache")
}

companion object {
private const val TAG = "WearStatePublisher"
private const val MAX_WEAR_LYRIC_LINES = 180
Expand All @@ -74,6 +91,7 @@ class WearStatePublisher @Inject constructor(
* Clear state from the Data Layer (e.g. when service is destroyed).
*/
fun clearState() {
clearCache()
scope.launch {
try {
val request = PutDataMapRequest.create(WearDataPaths.PLAYER_STATE).apply {
Expand Down Expand Up @@ -125,9 +143,8 @@ class WearStatePublisher @Inject constructor(
dataMap.putString(WearDataPaths.KEY_STATE_JSON, stateJson)
dataMap.putLong(WearDataPaths.KEY_TIMESTAMP, System.currentTimeMillis())

// Attach album art as Asset if available
val wearArtBytes = resolveArtworkBytesForWear(playerInfo)
val artAsset = createAlbumArtAsset(wearArtBytes)
// Attach album art as Asset if available (cached to prevent duplicate processing)
val artAsset = getOrCreateWearArtworkAsset(playerInfo)
if (artAsset != null) {
dataMap.putAsset(WearDataPaths.KEY_ALBUM_ART, artAsset)
} else {
Expand All @@ -139,6 +156,45 @@ class WearStatePublisher @Inject constructor(
Timber.tag(TAG).d("Published state to Wear: ${wearState.songTitle} (playing=${wearState.isPlaying})")
}

private fun getOrCreateWearArtworkAsset(playerInfo: PlayerInfo): Asset? {
val uriString = playerInfo.albumArtUri
val rawBitmapData = playerInfo.albumArtBitmapData

synchronized(artworkCacheLock) {
val uriMatches = uriString == lastArtworkUri
val dataMatches = if (rawBitmapData == null && lastRawBitmapData == null) {
true
} else if (rawBitmapData != null && lastRawBitmapData != null) {
rawBitmapData === lastRawBitmapData || rawBitmapData.contentEquals(lastRawBitmapData)
} else {
false
}
if (uriMatches && dataMatches) {
return cachedWearArtAsset
}
}

val sanitizedBytes = resolveArtworkBytesForWear(playerInfo)
val asset = if (sanitizedBytes != null) {
try {
Asset.createFromBytes(sanitizedBytes)
} catch (e: Exception) {
Timber.tag(TAG).w(e, "Failed to create album art asset")
null
}
} else {
null
}

synchronized(artworkCacheLock) {
lastArtworkUri = uriString
lastRawBitmapData = rawBitmapData
cachedWearArtAsset = asset
}

return asset
}

private fun resolveArtworkBytesForWear(playerInfo: PlayerInfo): ByteArray? {
val uriString = playerInfo.albumArtUri
if (!uriString.isNullOrBlank()) {
Expand Down Expand Up @@ -170,23 +226,6 @@ class WearStatePublisher @Inject constructor(
)
}

/**
* Compress album art to a JPEG suitable for full-screen watch display.
* Uses bounded downscale to preserve sharpness while keeping payload reasonable.
*/
private fun createAlbumArtAsset(artBitmapData: ByteArray?): Asset? {
val boundedBytes = ArtworkTransportSanitizer.sanitizeEncodedBytes(
data = artBitmapData,
config = ArtworkTransportSanitizer.WEAR_CONFIG,
) ?: return null
return try {
Asset.createFromBytes(boundedBytes)
} catch (e: Exception) {
Timber.tag(TAG).w(e, "Failed to create album art asset")
null
}
}

private fun downloadAndSanitizeRemoteArtwork(uriString: String): ByteArray? {
var connection: HttpURLConnection? = null
return try {
Expand Down
Loading