Verify native notification artwork with real JPEG regression
This commit is contained in:
@@ -58,6 +58,38 @@ class CompanionAudioTest {
|
|||||||
} finally { CompanionAudioBridge.release(web); lifecycle.destroy(); web.destroy() }
|
} 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() {
|
@Test fun diagnosticReportExcludesPrivateMessagesAndRecordsRejectionStage() {
|
||||||
val app = RuntimeEnvironment.getApplication()
|
val app = RuntimeEnvironment.getApplication()
|
||||||
val web = android.webkit.WebView(app)
|
val web = android.webkit.WebView(app)
|
||||||
@@ -116,3 +148,10 @@ class CompanionAudioTest {
|
|||||||
} finally { lifecycle.destroy() }
|
} 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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,10 +124,27 @@ Evidence:
|
|||||||
never produced a thumbnail; corrected composable produced a real JPEG. Browser
|
never produced a thumbnail; corrected composable produced a real JPEG. Browser
|
||||||
checks also verified no cross-origin cookie, no followed redirect, and playback
|
checks also verified no cross-origin cookie, no followed redirect, and playback
|
||||||
remained active after the optional image error.
|
remained active after the optional image error.
|
||||||
- No native runtime or APK version change. A positive native JPEG/notification/
|
- No native runtime or APK version change. The focused positive native JPEG
|
||||||
metadata regression is prepared separately and awaits its coordinated test slot.
|
regression passed on SDK 28 and 35 (2 tests, zero failures/errors/skips). It
|
||||||
|
verifies the actual service places a real JPEG in the notification large icon
|
||||||
|
and media-description bitmap, retains it on position-only updates, and clears
|
||||||
|
it for an explicitly artless following song. Initial test compilation used
|
||||||
|
unavailable Java desktop ImageIO APIs; replacing that test-only fixture with a
|
||||||
|
fixed Chromium-generated JPEG resolved the harness issue. Production Kotlin
|
||||||
|
remained unchanged/up-to-date. All owned test JVMs exited after success.
|
||||||
- This identifies and fixes a real artwork boundary defect, not the operator's
|
- This identifies and fixes a real artwork boundary defect, not the operator's
|
||||||
exact cover URL. Physical notification-artwork acceptance remains open until
|
exact cover URL. Physical notification-artwork acceptance remains open until
|
||||||
the qualified UI reaches the node and the operator verifies it.
|
the qualified UI reaches the node and the operator verifies it.
|
||||||
|
|
||||||
Evidence folder: `~/.local/state/archipelago/release-qualification/companion-artwork-20261007/`.
|
Evidence folder: `~/.local/state/archipelago/release-qualification/companion-artwork-20261007/`.
|
||||||
|
|
||||||
|
## Actual app artwork access limitation
|
||||||
|
|
||||||
|
A separate read-only Chromium probe retained the managed node-demo-v4v iframe,
|
||||||
|
but the actual app required Nostr login before initializing its media catalog.
|
||||||
|
With signing and payments blocked, the probe timed out before obtaining a cover
|
||||||
|
URL. No image access/CORS policy was bypassed, no credentials were forwarded,
|
||||||
|
and no real app artwork was decoded in that probe. The successful browser A/B
|
||||||
|
fixture and native JPEG test establish the corrected pipeline, not that the
|
||||||
|
operator's particular cover is anonymously accessible. Physical artwork acceptance
|
||||||
|
remains pending after the qualified UI deployment; APK 0.5.37 stays unchanged.
|
||||||
|
|||||||
Reference in New Issue
Block a user