Verify native notification artwork with real JPEG regression

This commit is contained in:
archipelago
2026-10-07 19:00:20 -04:00
parent b0b95810e0
commit 60bd728a04
2 changed files with 58 additions and 2 deletions
@@ -58,6 +58,38 @@ class CompanionAudioTest {
} finally { CompanionAudioBridge.release(web); lifecycle.destroy(); web.destroy() }
}
@Test
@Config(shadows = [RecordingAudioMediaSession::class])
fun jpegArtworkReachesNotificationAndMediaDescription() {
// Real 16x16 JPEG generated by Chromium canvas, independent of Android bitmap shadows.
val artwork = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJVAA//Z"
assertNotNull(decodeArtwork(artwork))
val app = RuntimeEnvironment.getApplication()
val web = android.webkit.WebView(app); web.loadUrl("https://node.test/cloud")
val payload = state().put("session", "32345678-1234-1234-1234-123456789abc").put("artwork", artwork)
CompanionAudioBridge.receive(web, payload.toString(), "https://node.test", true, setOf("https://node.test")) {}
val lifecycle = Robolectric.buildService(CompanionAudioService::class.java).create()
try {
lifecycle.get().onStartCommand(null, 0, 1)
val notification = Shadows.shadowOf(lifecycle.get()).lastForegroundNotification
assertNotNull(notification.getLargeIcon())
// Robolectric's MediaController does not read MediaSession metadata;
// capture the actual service's setMetadata call instead.
val metadata = RecordingAudioMediaSession.metadata!!
assertNotNull(metadata.getBitmap(android.media.MediaMetadata.METADATA_KEY_ALBUM_ART))
assertNotNull(metadata.description.iconBitmap)
// Position-only refresh must retain the same thumbnail.
CompanionAudioBridge.receive(web, state().put("session", payload.getString("session")).put("sequence", 2).toString(),
"https://node.test", true, setOf("https://node.test")) {}
assertNotNull(Shadows.shadowOf(lifecycle.get()).lastForegroundNotification.getLargeIcon())
// A following song without art must not keep the previous cover.
CompanionAudioBridge.receive(web, state().put("session", payload.getString("session")).put("sequence", 3).put("artwork", "").toString(),
"https://node.test", true, setOf("https://node.test")) {}
assertNull(Shadows.shadowOf(lifecycle.get()).lastForegroundNotification.getLargeIcon())
assertNull(RecordingAudioMediaSession.metadata!!.description.iconBitmap)
} finally { CompanionAudioBridge.release(web); lifecycle.destroy(); web.destroy() }
}
@Test fun diagnosticReportExcludesPrivateMessagesAndRecordsRejectionStage() {
val app = RuntimeEnvironment.getApplication()
val web = android.webkit.WebView(app)
@@ -116,3 +148,10 @@ class CompanionAudioTest {
} finally { lifecycle.destroy() }
}
}
@org.robolectric.annotation.Implements(android.media.session.MediaSession::class)
class RecordingAudioMediaSession : org.robolectric.shadows.ShadowMediaSession() {
companion object { var metadata: android.media.MediaMetadata? = null }
@org.robolectric.annotation.Implementation
fun setMetadata(value: android.media.MediaMetadata) { metadata = value }
}